feat: controlnet example
Browse files- .env.example +2 -1
- bun.lockb +0 -0
- package.json +2 -0
- src/app/page.tsx +194 -6
- src/components/ui/select.tsx +160 -0
- src/components/ui/separator.tsx +31 -0
- src/lib/comfy-deploy.ts +17 -12
- src/server/generate.tsx +11 -1
.env.example
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
COMFY_API_URL="http://127.0.0.1:3000"
|
| 2 |
COMFY_API_TOKEN=""
|
| 3 |
COMFY_DEPLOYMENT_ID=""
|
| 4 |
-
COMFY_DEPLOYMENT_ID_IMG_2_IMG=""
|
|
|
|
|
|
| 1 |
COMFY_API_URL="http://127.0.0.1:3000"
|
| 2 |
COMFY_API_TOKEN=""
|
| 3 |
COMFY_DEPLOYMENT_ID=""
|
| 4 |
+
COMFY_DEPLOYMENT_ID_IMG_2_IMG=""
|
| 5 |
+
COMFY_DEPLOYMENT_ID_CONTROLNET=""
|
bun.lockb
CHANGED
|
Binary files a/bun.lockb and b/bun.lockb differ
|
|
|
package.json
CHANGED
|
@@ -11,6 +11,8 @@
|
|
| 11 |
"dependencies": {
|
| 12 |
"@hookform/resolvers": "^3.3.4",
|
| 13 |
"@radix-ui/react-label": "^2.0.2",
|
|
|
|
|
|
|
| 14 |
"@radix-ui/react-slot": "^1.0.2",
|
| 15 |
"@radix-ui/react-tabs": "^1.0.4",
|
| 16 |
"class-variance-authority": "^0.7.0",
|
|
|
|
| 11 |
"dependencies": {
|
| 12 |
"@hookform/resolvers": "^3.3.4",
|
| 13 |
"@radix-ui/react-label": "^2.0.2",
|
| 14 |
+
"@radix-ui/react-select": "^2.0.0",
|
| 15 |
+
"@radix-ui/react-separator": "^1.0.3",
|
| 16 |
"@radix-ui/react-slot": "^1.0.2",
|
| 17 |
"@radix-ui/react-tabs": "^1.0.4",
|
| 18 |
"class-variance-authority": "^0.7.0",
|
src/app/page.tsx
CHANGED
|
@@ -10,19 +10,32 @@ import {
|
|
| 10 |
checkStatus,
|
| 11 |
generate,
|
| 12 |
generate_img,
|
|
|
|
| 13 |
getUploadUrl,
|
| 14 |
} from "@/server/generate";
|
| 15 |
import { useEffect, useState } from "react";
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
| 18 |
|
| 19 |
export default function Page() {
|
| 20 |
return (
|
| 21 |
<main className="flex min-h-screen flex-col items-center justify-between mt-10">
|
| 22 |
-
<Tabs defaultValue="txt2img" className="w-full max-w-[
|
| 23 |
-
<TabsList className="grid w-full grid-cols-
|
| 24 |
<TabsTrigger value="txt2img">txt2img</TabsTrigger>
|
| 25 |
<TabsTrigger value="img2img">img2img</TabsTrigger>
|
|
|
|
| 26 |
</TabsList>
|
| 27 |
<TabsContent value="txt2img">
|
| 28 |
<Txt2img />
|
|
@@ -30,6 +43,9 @@ export default function Page() {
|
|
| 30 |
<TabsContent value="img2img">
|
| 31 |
<Img2img />
|
| 32 |
</TabsContent>
|
|
|
|
|
|
|
|
|
|
| 33 |
</Tabs>
|
| 34 |
</main>
|
| 35 |
);
|
|
@@ -60,7 +76,7 @@ function Txt2img() {
|
|
| 60 |
}, [runId]);
|
| 61 |
|
| 62 |
return (
|
| 63 |
-
<Card className="w-full max-w-[
|
| 64 |
<CardHeader>
|
| 65 |
Comfy Deploy - Vector Line Art Tool
|
| 66 |
<div className="text-xs text-foreground opacity-50">
|
|
@@ -107,7 +123,8 @@ function Txt2img() {
|
|
| 107 |
className="w-full h-full object-contain"
|
| 108 |
src={image}
|
| 109 |
alt="Generated image"
|
| 110 |
-
|
|
|
|
| 111 |
)}
|
| 112 |
{!image && status && (
|
| 113 |
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center gap-2">
|
|
@@ -152,7 +169,7 @@ function Img2img() {
|
|
| 152 |
}, [runId]);
|
| 153 |
|
| 154 |
return (
|
| 155 |
-
<Card className="w-full max-w-[
|
| 156 |
<CardHeader>Comfy Deploy - Scribble to Anime Girl</CardHeader>
|
| 157 |
<CardContent>
|
| 158 |
<form
|
|
@@ -215,7 +232,8 @@ function Img2img() {
|
|
| 215 |
className="w-full h-full object-contain"
|
| 216 |
src={image}
|
| 217 |
alt="Generated image"
|
| 218 |
-
|
|
|
|
| 219 |
)}
|
| 220 |
{!image && status && (
|
| 221 |
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center gap-2">
|
|
@@ -229,3 +247,173 @@ function Img2img() {
|
|
| 229 |
</Card>
|
| 230 |
);
|
| 231 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
checkStatus,
|
| 11 |
generate,
|
| 12 |
generate_img,
|
| 13 |
+
generate_img_with_controlnet,
|
| 14 |
getUploadUrl,
|
| 15 |
} from "@/server/generate";
|
| 16 |
import { useEffect, useState } from "react";
|
| 17 |
|
| 18 |
+
import {
|
| 19 |
+
Select,
|
| 20 |
+
SelectContent,
|
| 21 |
+
SelectGroup,
|
| 22 |
+
SelectItem,
|
| 23 |
+
SelectLabel,
|
| 24 |
+
SelectTrigger,
|
| 25 |
+
SelectValue,
|
| 26 |
+
} from "@/components/ui/select";
|
| 27 |
+
import { Separator } from "@/components/ui/separator";
|
| 28 |
+
|
| 29 |
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
| 30 |
|
| 31 |
export default function Page() {
|
| 32 |
return (
|
| 33 |
<main className="flex min-h-screen flex-col items-center justify-between mt-10">
|
| 34 |
+
<Tabs defaultValue="txt2img" className="w-full max-w-[600px]">
|
| 35 |
+
<TabsList className="grid w-full grid-cols-3">
|
| 36 |
<TabsTrigger value="txt2img">txt2img</TabsTrigger>
|
| 37 |
<TabsTrigger value="img2img">img2img</TabsTrigger>
|
| 38 |
+
<TabsTrigger value="controlpose">Controlpose</TabsTrigger>
|
| 39 |
</TabsList>
|
| 40 |
<TabsContent value="txt2img">
|
| 41 |
<Txt2img />
|
|
|
|
| 43 |
<TabsContent value="img2img">
|
| 44 |
<Img2img />
|
| 45 |
</TabsContent>
|
| 46 |
+
<TabsContent value="controlpose">
|
| 47 |
+
<OpenposeToImage />
|
| 48 |
+
</TabsContent>
|
| 49 |
</Tabs>
|
| 50 |
</main>
|
| 51 |
);
|
|
|
|
| 76 |
}, [runId]);
|
| 77 |
|
| 78 |
return (
|
| 79 |
+
<Card className="w-full max-w-[600px]">
|
| 80 |
<CardHeader>
|
| 81 |
Comfy Deploy - Vector Line Art Tool
|
| 82 |
<div className="text-xs text-foreground opacity-50">
|
|
|
|
| 123 |
className="w-full h-full object-contain"
|
| 124 |
src={image}
|
| 125 |
alt="Generated image"
|
| 126 |
+
>
|
| 127 |
+
</img>
|
| 128 |
)}
|
| 129 |
{!image && status && (
|
| 130 |
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center gap-2">
|
|
|
|
| 169 |
}, [runId]);
|
| 170 |
|
| 171 |
return (
|
| 172 |
+
<Card className="w-full max-w-[600px]">
|
| 173 |
<CardHeader>Comfy Deploy - Scribble to Anime Girl</CardHeader>
|
| 174 |
<CardContent>
|
| 175 |
<form
|
|
|
|
| 232 |
className="w-full h-full object-contain"
|
| 233 |
src={image}
|
| 234 |
alt="Generated image"
|
| 235 |
+
>
|
| 236 |
+
</img>
|
| 237 |
)}
|
| 238 |
{!image && status && (
|
| 239 |
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center gap-2">
|
|
|
|
| 247 |
</Card>
|
| 248 |
);
|
| 249 |
}
|
| 250 |
+
|
| 251 |
+
const poses = {
|
| 252 |
+
arms_on_hips: {
|
| 253 |
+
url:
|
| 254 |
+
"https://pub-6230db03dc3a4861a9c3e55145ceda44.r2.dev/openpose-pose%20(1).png",
|
| 255 |
+
name: "Arms on Hips",
|
| 256 |
+
},
|
| 257 |
+
waving: {
|
| 258 |
+
url:
|
| 259 |
+
"https://pub-6230db03dc3a4861a9c3e55145ceda44.r2.dev/openpose-pose%20(2).png",
|
| 260 |
+
name: "Waving",
|
| 261 |
+
},
|
| 262 |
+
legs_together_sideways: {
|
| 263 |
+
url:
|
| 264 |
+
"https://pub-6230db03dc3a4861a9c3e55145ceda44.r2.dev/openpose-pose%20(3).png",
|
| 265 |
+
name: "Legs together, body at an angle",
|
| 266 |
+
},
|
| 267 |
+
excited_jump: {
|
| 268 |
+
url:
|
| 269 |
+
"https://pub-6230db03dc3a4861a9c3e55145ceda44.r2.dev/openpose-pose%20(4).png",
|
| 270 |
+
name: "excited jump",
|
| 271 |
+
},
|
| 272 |
+
pointing_to_the_stars: {
|
| 273 |
+
url:
|
| 274 |
+
"https://pub-6230db03dc3a4861a9c3e55145ceda44.r2.dev/openpose-pose%20(5).png",
|
| 275 |
+
name: "Pointing to the stars",
|
| 276 |
+
},
|
| 277 |
+
};
|
| 278 |
+
|
| 279 |
+
function OpenposeToImage() {
|
| 280 |
+
const [prompt, setPrompt] = useState("");
|
| 281 |
+
const [poseImageUrl, setPoseImageUrl] = useState(
|
| 282 |
+
"https://pub-6230db03dc3a4861a9c3e55145ceda44.r2.dev/openpose-pose%20(1).png",
|
| 283 |
+
);
|
| 284 |
+
const [poseLoading, setPoseLoading] = useState(false);
|
| 285 |
+
const [image, setImage] = useState("");
|
| 286 |
+
const [loading, setLoading] = useState(false);
|
| 287 |
+
const [runId, setRunId] = useState("");
|
| 288 |
+
const [status, setStatus] = useState<string>();
|
| 289 |
+
|
| 290 |
+
const handleSelectChange = (value: keyof typeof poses) => {
|
| 291 |
+
setPoseImageUrl(poses[value].url); // Update image based on selection
|
| 292 |
+
};
|
| 293 |
+
|
| 294 |
+
// Polling in frontend to check for the
|
| 295 |
+
useEffect(() => {
|
| 296 |
+
if (!runId) return;
|
| 297 |
+
const interval = setInterval(() => {
|
| 298 |
+
checkStatus(runId).then((res) => {
|
| 299 |
+
if (res) setStatus(res.status);
|
| 300 |
+
if (res && res.status === "success") {
|
| 301 |
+
console.log(res.outputs[0]?.data);
|
| 302 |
+
setImage(res.outputs[0]?.data?.images[0].url);
|
| 303 |
+
setLoading(false);
|
| 304 |
+
clearInterval(interval);
|
| 305 |
+
}
|
| 306 |
+
});
|
| 307 |
+
}, 2000);
|
| 308 |
+
return () => clearInterval(interval);
|
| 309 |
+
}, [runId]);
|
| 310 |
+
|
| 311 |
+
return (
|
| 312 |
+
<Card className="w-full max-w-[600px]">
|
| 313 |
+
<CardHeader>
|
| 314 |
+
Comfy Deploy - Pose Creator Tool
|
| 315 |
+
<div className="text-xs text-foreground opacity-50">
|
| 316 |
+
OpenPose -{" "}
|
| 317 |
+
<a href="https://civitai.com/models/13647/super-pose-book-vol1-controlnet">
|
| 318 |
+
pose book
|
| 319 |
+
</a>
|
| 320 |
+
</div>
|
| 321 |
+
</CardHeader>
|
| 322 |
+
<CardContent>
|
| 323 |
+
<form
|
| 324 |
+
className="grid w-full items-center gap-1.5"
|
| 325 |
+
onSubmit={(e) => {
|
| 326 |
+
if (loading) return;
|
| 327 |
+
|
| 328 |
+
e.preventDefault();
|
| 329 |
+
setLoading(true);
|
| 330 |
+
generate_img_with_controlnet(poseImageUrl, prompt).then((res) => {
|
| 331 |
+
console.log('here', res);
|
| 332 |
+
if (!res) {
|
| 333 |
+
setStatus("error");
|
| 334 |
+
setLoading(false);
|
| 335 |
+
return;
|
| 336 |
+
}
|
| 337 |
+
setRunId(res.run_id);
|
| 338 |
+
});
|
| 339 |
+
setStatus("preparing");
|
| 340 |
+
}}
|
| 341 |
+
>
|
| 342 |
+
<Select
|
| 343 |
+
defaultValue={"Arms on Hips"}
|
| 344 |
+
onValueChange={(value) => {
|
| 345 |
+
handleSelectChange(value as keyof typeof poses);
|
| 346 |
+
setPoseLoading(true); // Start loading when a new pose is selected
|
| 347 |
+
}}
|
| 348 |
+
>
|
| 349 |
+
<Label htmlFor="picture">Pose</Label>
|
| 350 |
+
<SelectTrigger className="w-[180px]">
|
| 351 |
+
<SelectValue placeholder="Select a Pose" />
|
| 352 |
+
</SelectTrigger>
|
| 353 |
+
<SelectContent>
|
| 354 |
+
<SelectGroup>
|
| 355 |
+
<SelectLabel>Poses</SelectLabel>
|
| 356 |
+
{Object.entries(poses).map(([poseName, attr]) => (
|
| 357 |
+
<SelectItem key={poseName} value={poseName}>
|
| 358 |
+
{attr.name}
|
| 359 |
+
</SelectItem>
|
| 360 |
+
))}
|
| 361 |
+
</SelectGroup>
|
| 362 |
+
</SelectContent>
|
| 363 |
+
</Select>
|
| 364 |
+
<Label htmlFor="picture">Image prompt</Label>
|
| 365 |
+
<Input
|
| 366 |
+
id="picture"
|
| 367 |
+
type="text"
|
| 368 |
+
value={prompt}
|
| 369 |
+
onChange={(e) => setPrompt(e.target.value)}
|
| 370 |
+
/>
|
| 371 |
+
<Button type="submit" className="flex gap-2" disabled={loading}>
|
| 372 |
+
Generate {loading && <LoadingIcon />}
|
| 373 |
+
</Button>
|
| 374 |
+
|
| 375 |
+
<div className="flex gap-4">
|
| 376 |
+
<div className="w-full rounded-lg relative">
|
| 377 |
+
{/* Pose Image */}
|
| 378 |
+
{poseLoading && (
|
| 379 |
+
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
| 380 |
+
<LoadingIcon />
|
| 381 |
+
</div>
|
| 382 |
+
)}
|
| 383 |
+
{poseImageUrl && (
|
| 384 |
+
<img
|
| 385 |
+
className="w-full h-full object-contain"
|
| 386 |
+
src={poseImageUrl}
|
| 387 |
+
alt="Selected pose"
|
| 388 |
+
onLoad={() => setPoseLoading(false)}
|
| 389 |
+
>
|
| 390 |
+
</img>
|
| 391 |
+
)}
|
| 392 |
+
</div>
|
| 393 |
+
<Separator
|
| 394 |
+
orientation="vertical"
|
| 395 |
+
className="border-gray-200"
|
| 396 |
+
decorative
|
| 397 |
+
/>
|
| 398 |
+
<div className="border border-gray-200 w-full square h-[400px] rounded-lg relative">
|
| 399 |
+
{!loading && image && (
|
| 400 |
+
<img
|
| 401 |
+
className="w-full h-full object-contain"
|
| 402 |
+
src={image}
|
| 403 |
+
alt="Generated image"
|
| 404 |
+
>
|
| 405 |
+
</img>
|
| 406 |
+
)}
|
| 407 |
+
{!image && status && (
|
| 408 |
+
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center gap-2">
|
| 409 |
+
{status} <LoadingIcon />
|
| 410 |
+
</div>
|
| 411 |
+
)}
|
| 412 |
+
{loading && <Skeleton className="w-full h-full" />}
|
| 413 |
+
</div>
|
| 414 |
+
</div>
|
| 415 |
+
</form>
|
| 416 |
+
</CardContent>
|
| 417 |
+
</Card>
|
| 418 |
+
);
|
| 419 |
+
}
|
src/components/ui/select.tsx
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as SelectPrimitive from "@radix-ui/react-select"
|
| 5 |
+
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
| 6 |
+
|
| 7 |
+
import { cn } from "@/lib/utils"
|
| 8 |
+
|
| 9 |
+
const Select = SelectPrimitive.Root
|
| 10 |
+
|
| 11 |
+
const SelectGroup = SelectPrimitive.Group
|
| 12 |
+
|
| 13 |
+
const SelectValue = SelectPrimitive.Value
|
| 14 |
+
|
| 15 |
+
const SelectTrigger = React.forwardRef<
|
| 16 |
+
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
| 17 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
| 18 |
+
>(({ className, children, ...props }, ref) => (
|
| 19 |
+
<SelectPrimitive.Trigger
|
| 20 |
+
ref={ref}
|
| 21 |
+
className={cn(
|
| 22 |
+
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
| 23 |
+
className
|
| 24 |
+
)}
|
| 25 |
+
{...props}
|
| 26 |
+
>
|
| 27 |
+
{children}
|
| 28 |
+
<SelectPrimitive.Icon asChild>
|
| 29 |
+
<ChevronDown className="h-4 w-4 opacity-50" />
|
| 30 |
+
</SelectPrimitive.Icon>
|
| 31 |
+
</SelectPrimitive.Trigger>
|
| 32 |
+
))
|
| 33 |
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
| 34 |
+
|
| 35 |
+
const SelectScrollUpButton = React.forwardRef<
|
| 36 |
+
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
| 37 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
| 38 |
+
>(({ className, ...props }, ref) => (
|
| 39 |
+
<SelectPrimitive.ScrollUpButton
|
| 40 |
+
ref={ref}
|
| 41 |
+
className={cn(
|
| 42 |
+
"flex cursor-default items-center justify-center py-1",
|
| 43 |
+
className
|
| 44 |
+
)}
|
| 45 |
+
{...props}
|
| 46 |
+
>
|
| 47 |
+
<ChevronUp className="h-4 w-4" />
|
| 48 |
+
</SelectPrimitive.ScrollUpButton>
|
| 49 |
+
))
|
| 50 |
+
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
| 51 |
+
|
| 52 |
+
const SelectScrollDownButton = React.forwardRef<
|
| 53 |
+
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
| 54 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
| 55 |
+
>(({ className, ...props }, ref) => (
|
| 56 |
+
<SelectPrimitive.ScrollDownButton
|
| 57 |
+
ref={ref}
|
| 58 |
+
className={cn(
|
| 59 |
+
"flex cursor-default items-center justify-center py-1",
|
| 60 |
+
className
|
| 61 |
+
)}
|
| 62 |
+
{...props}
|
| 63 |
+
>
|
| 64 |
+
<ChevronDown className="h-4 w-4" />
|
| 65 |
+
</SelectPrimitive.ScrollDownButton>
|
| 66 |
+
))
|
| 67 |
+
SelectScrollDownButton.displayName =
|
| 68 |
+
SelectPrimitive.ScrollDownButton.displayName
|
| 69 |
+
|
| 70 |
+
const SelectContent = React.forwardRef<
|
| 71 |
+
React.ElementRef<typeof SelectPrimitive.Content>,
|
| 72 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
| 73 |
+
>(({ className, children, position = "popper", ...props }, ref) => (
|
| 74 |
+
<SelectPrimitive.Portal>
|
| 75 |
+
<SelectPrimitive.Content
|
| 76 |
+
ref={ref}
|
| 77 |
+
className={cn(
|
| 78 |
+
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
| 79 |
+
position === "popper" &&
|
| 80 |
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
| 81 |
+
className
|
| 82 |
+
)}
|
| 83 |
+
position={position}
|
| 84 |
+
{...props}
|
| 85 |
+
>
|
| 86 |
+
<SelectScrollUpButton />
|
| 87 |
+
<SelectPrimitive.Viewport
|
| 88 |
+
className={cn(
|
| 89 |
+
"p-1",
|
| 90 |
+
position === "popper" &&
|
| 91 |
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
| 92 |
+
)}
|
| 93 |
+
>
|
| 94 |
+
{children}
|
| 95 |
+
</SelectPrimitive.Viewport>
|
| 96 |
+
<SelectScrollDownButton />
|
| 97 |
+
</SelectPrimitive.Content>
|
| 98 |
+
</SelectPrimitive.Portal>
|
| 99 |
+
))
|
| 100 |
+
SelectContent.displayName = SelectPrimitive.Content.displayName
|
| 101 |
+
|
| 102 |
+
const SelectLabel = React.forwardRef<
|
| 103 |
+
React.ElementRef<typeof SelectPrimitive.Label>,
|
| 104 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
| 105 |
+
>(({ className, ...props }, ref) => (
|
| 106 |
+
<SelectPrimitive.Label
|
| 107 |
+
ref={ref}
|
| 108 |
+
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
| 109 |
+
{...props}
|
| 110 |
+
/>
|
| 111 |
+
))
|
| 112 |
+
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
| 113 |
+
|
| 114 |
+
const SelectItem = React.forwardRef<
|
| 115 |
+
React.ElementRef<typeof SelectPrimitive.Item>,
|
| 116 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
| 117 |
+
>(({ className, children, ...props }, ref) => (
|
| 118 |
+
<SelectPrimitive.Item
|
| 119 |
+
ref={ref}
|
| 120 |
+
className={cn(
|
| 121 |
+
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
| 122 |
+
className
|
| 123 |
+
)}
|
| 124 |
+
{...props}
|
| 125 |
+
>
|
| 126 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
| 127 |
+
<SelectPrimitive.ItemIndicator>
|
| 128 |
+
<Check className="h-4 w-4" />
|
| 129 |
+
</SelectPrimitive.ItemIndicator>
|
| 130 |
+
</span>
|
| 131 |
+
|
| 132 |
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
| 133 |
+
</SelectPrimitive.Item>
|
| 134 |
+
))
|
| 135 |
+
SelectItem.displayName = SelectPrimitive.Item.displayName
|
| 136 |
+
|
| 137 |
+
const SelectSeparator = React.forwardRef<
|
| 138 |
+
React.ElementRef<typeof SelectPrimitive.Separator>,
|
| 139 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
| 140 |
+
>(({ className, ...props }, ref) => (
|
| 141 |
+
<SelectPrimitive.Separator
|
| 142 |
+
ref={ref}
|
| 143 |
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
| 144 |
+
{...props}
|
| 145 |
+
/>
|
| 146 |
+
))
|
| 147 |
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
| 148 |
+
|
| 149 |
+
export {
|
| 150 |
+
Select,
|
| 151 |
+
SelectGroup,
|
| 152 |
+
SelectValue,
|
| 153 |
+
SelectTrigger,
|
| 154 |
+
SelectContent,
|
| 155 |
+
SelectLabel,
|
| 156 |
+
SelectItem,
|
| 157 |
+
SelectSeparator,
|
| 158 |
+
SelectScrollUpButton,
|
| 159 |
+
SelectScrollDownButton,
|
| 160 |
+
}
|
src/components/ui/separator.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
const Separator = React.forwardRef<
|
| 9 |
+
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
| 10 |
+
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
| 11 |
+
>(
|
| 12 |
+
(
|
| 13 |
+
{ className, orientation = "horizontal", decorative = true, ...props },
|
| 14 |
+
ref
|
| 15 |
+
) => (
|
| 16 |
+
<SeparatorPrimitive.Root
|
| 17 |
+
ref={ref}
|
| 18 |
+
decorative={decorative}
|
| 19 |
+
orientation={orientation}
|
| 20 |
+
className={cn(
|
| 21 |
+
"shrink-0 bg-border",
|
| 22 |
+
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
| 23 |
+
className
|
| 24 |
+
)}
|
| 25 |
+
{...props}
|
| 26 |
+
/>
|
| 27 |
+
)
|
| 28 |
+
)
|
| 29 |
+
Separator.displayName = SeparatorPrimitive.Root.displayName
|
| 30 |
+
|
| 31 |
+
export { Separator }
|
src/lib/comfy-deploy.ts
CHANGED
|
@@ -6,11 +6,11 @@ const runTypes = z.object({
|
|
| 6 |
|
| 7 |
const runOutputTypes = z.object({
|
| 8 |
id: z.string(),
|
| 9 |
-
status: z.enum(["success", "failed", "running", "uploading"]),
|
| 10 |
outputs: z.array(
|
| 11 |
z.object({
|
| 12 |
data: z.any(),
|
| 13 |
-
})
|
| 14 |
),
|
| 15 |
});
|
| 16 |
|
|
@@ -18,15 +18,16 @@ const uploadFileTypes = z.object({
|
|
| 18 |
upload_url: z.string(),
|
| 19 |
file_id: z.string(),
|
| 20 |
download_url: z.string(),
|
| 21 |
-
})
|
| 22 |
|
| 23 |
export class ComfyDeployClient {
|
| 24 |
apiBase: string = "https://www.comfydeploy.com/api";
|
| 25 |
apiToken: string;
|
| 26 |
|
| 27 |
constructor({ apiBase, apiToken }: { apiBase?: string; apiToken: string }) {
|
| 28 |
-
if (apiBase)
|
| 29 |
this.apiBase = `${apiBase}/api`;
|
|
|
|
| 30 |
this.apiToken = apiToken;
|
| 31 |
}
|
| 32 |
|
|
@@ -47,12 +48,16 @@ export class ComfyDeployClient {
|
|
| 47 |
deployment_id: deployment_id,
|
| 48 |
inputs: inputs,
|
| 49 |
}),
|
| 50 |
-
cache: "no-store"
|
| 51 |
})
|
| 52 |
-
.then((response) =>
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
.catch((err) => {
|
| 55 |
-
console.error(err);
|
| 56 |
return null;
|
| 57 |
});
|
| 58 |
}
|
|
@@ -64,7 +69,7 @@ export class ComfyDeployClient {
|
|
| 64 |
"Content-Type": "application/json",
|
| 65 |
authorization: `Bearer ${this.apiToken}`,
|
| 66 |
},
|
| 67 |
-
cache: "no-store"
|
| 68 |
})
|
| 69 |
.then((response) => response.json())
|
| 70 |
.then((json) => runOutputTypes.parse(json))
|
|
@@ -110,13 +115,13 @@ export class ComfyDeployClient {
|
|
| 110 |
};
|
| 111 |
const url = new URL(`${this.apiBase}/upload-url`);
|
| 112 |
url.search = new URLSearchParams(obj).toString();
|
| 113 |
-
|
| 114 |
return await fetch(url.href, {
|
| 115 |
method: "GET",
|
| 116 |
headers: {
|
| 117 |
authorization: `Bearer ${this.apiToken}`,
|
| 118 |
},
|
| 119 |
-
cache: "no-store"
|
| 120 |
})
|
| 121 |
.then((response) => response.json())
|
| 122 |
.then((json) => uploadFileTypes.parse(json))
|
|
@@ -125,4 +130,4 @@ export class ComfyDeployClient {
|
|
| 125 |
return null;
|
| 126 |
});
|
| 127 |
}
|
| 128 |
-
}
|
|
|
|
| 6 |
|
| 7 |
const runOutputTypes = z.object({
|
| 8 |
id: z.string(),
|
| 9 |
+
status: z.enum(["success", "failed", "running", "uploading", "not-started"]),
|
| 10 |
outputs: z.array(
|
| 11 |
z.object({
|
| 12 |
data: z.any(),
|
| 13 |
+
}),
|
| 14 |
),
|
| 15 |
});
|
| 16 |
|
|
|
|
| 18 |
upload_url: z.string(),
|
| 19 |
file_id: z.string(),
|
| 20 |
download_url: z.string(),
|
| 21 |
+
});
|
| 22 |
|
| 23 |
export class ComfyDeployClient {
|
| 24 |
apiBase: string = "https://www.comfydeploy.com/api";
|
| 25 |
apiToken: string;
|
| 26 |
|
| 27 |
constructor({ apiBase, apiToken }: { apiBase?: string; apiToken: string }) {
|
| 28 |
+
if (apiBase) {
|
| 29 |
this.apiBase = `${apiBase}/api`;
|
| 30 |
+
}
|
| 31 |
this.apiToken = apiToken;
|
| 32 |
}
|
| 33 |
|
|
|
|
| 48 |
deployment_id: deployment_id,
|
| 49 |
inputs: inputs,
|
| 50 |
}),
|
| 51 |
+
cache: "no-store",
|
| 52 |
})
|
| 53 |
+
.then((response) => {
|
| 54 |
+
console.log('response', response)
|
| 55 |
+
return response.json()})
|
| 56 |
+
.then((json) => {
|
| 57 |
+
console.log('json', json)
|
| 58 |
+
return runTypes.parse(json)})
|
| 59 |
.catch((err) => {
|
| 60 |
+
console.error('err', err);
|
| 61 |
return null;
|
| 62 |
});
|
| 63 |
}
|
|
|
|
| 69 |
"Content-Type": "application/json",
|
| 70 |
authorization: `Bearer ${this.apiToken}`,
|
| 71 |
},
|
| 72 |
+
cache: "no-store",
|
| 73 |
})
|
| 74 |
.then((response) => response.json())
|
| 75 |
.then((json) => runOutputTypes.parse(json))
|
|
|
|
| 115 |
};
|
| 116 |
const url = new URL(`${this.apiBase}/upload-url`);
|
| 117 |
url.search = new URLSearchParams(obj).toString();
|
| 118 |
+
|
| 119 |
return await fetch(url.href, {
|
| 120 |
method: "GET",
|
| 121 |
headers: {
|
| 122 |
authorization: `Bearer ${this.apiToken}`,
|
| 123 |
},
|
| 124 |
+
cache: "no-store",
|
| 125 |
})
|
| 126 |
.then((response) => response.json())
|
| 127 |
.then((json) => uploadFileTypes.parse(json))
|
|
|
|
| 130 |
return null;
|
| 131 |
});
|
| 132 |
}
|
| 133 |
+
}
|
src/server/generate.tsx
CHANGED
|
@@ -25,6 +25,16 @@ export async function generate_img(input_image: string){
|
|
| 25 |
})
|
| 26 |
}
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
export async function checkStatus(run_id: string){
|
| 29 |
return await client.getRun(run_id)
|
| 30 |
}
|
|
@@ -35,4 +45,4 @@ export async function getUploadUrl(type: string, file_size: number){
|
|
| 35 |
} catch (error) {
|
| 36 |
console.log(error)
|
| 37 |
}
|
| 38 |
-
}
|
|
|
|
| 25 |
})
|
| 26 |
}
|
| 27 |
|
| 28 |
+
export async function generate_img_with_controlnet(input_openpose_url: string, prompt: string){
|
| 29 |
+
return await client.run({
|
| 30 |
+
deployment_id: process.env.COMFY_DEPLOYMENT_ID_CONTROLNET!,
|
| 31 |
+
inputs: {
|
| 32 |
+
"positive_prompt": prompt,
|
| 33 |
+
"openpose": input_openpose_url
|
| 34 |
+
}
|
| 35 |
+
})
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
export async function checkStatus(run_id: string){
|
| 39 |
return await client.getRun(run_id)
|
| 40 |
}
|
|
|
|
| 45 |
} catch (error) {
|
| 46 |
console.log(error)
|
| 47 |
}
|
| 48 |
+
}
|