BennyKok commited on
Commit
916c5da
·
1 Parent(s): 47d4f21
bun.lockb CHANGED
Binary files a/bun.lockb and b/bun.lockb differ
 
package.json CHANGED
@@ -12,6 +12,7 @@
12
  "@hookform/resolvers": "^3.3.4",
13
  "@radix-ui/react-label": "^2.0.2",
14
  "@radix-ui/react-slot": "^1.0.2",
 
15
  "class-variance-authority": "^0.7.0",
16
  "clsx": "^2.1.0",
17
  "lucide-react": "^0.309.0",
 
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",
17
  "clsx": "^2.1.0",
18
  "lucide-react": "^0.309.0",
src/app/page.tsx CHANGED
@@ -6,15 +6,41 @@ import { Card, CardContent, CardHeader } from "@/components/ui/card";
6
  import { Input } from "@/components/ui/input";
7
  import { Label } from "@/components/ui/label";
8
  import { Skeleton } from "@/components/ui/skeleton";
9
- import { checkStatus, generate } from "@/server/generate";
 
 
 
 
 
10
  import { useEffect, useState } from "react";
11
 
12
- export default function Home() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  const [prompt, setPrompt] = useState("");
14
  const [image, setImage] = useState("");
15
  const [loading, setLoading] = useState(false);
16
  const [runId, setRunId] = useState("");
17
- const [status, setStatus] = useState("preparing");
18
 
19
  // Polling in frontend to check for the
20
  useEffect(() => {
@@ -23,8 +49,8 @@ export default function Home() {
23
  checkStatus(runId).then((res) => {
24
  if (res) setStatus(res.status);
25
  if (res && res.status === "success") {
26
- console.log(res.outputs[0].data);
27
- setImage(res.outputs[0].data.images[0].url);
28
  setLoading(false);
29
  clearInterval(interval);
30
  }
@@ -34,62 +60,179 @@ export default function Home() {
34
  }, [runId]);
35
 
36
  return (
37
- <main className="flex min-h-screen flex-col items-center justify-between mt-10">
38
- <Card className="w-full max-w-[500px]">
39
- <CardHeader>
40
- Comfy Deploy - Vector Line Art Tool
41
- <div className="text-xs text-foreground opacity-50">
42
- Lora -{" "}
43
- <a href="https://civitai.com/models/256144/stick-line-vector-illustration">
44
- stick-line-vector-illustration
45
- </a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  </div>
47
- </CardHeader>
48
- <CardContent>
49
- <form
50
- className="grid w-full items-center gap-1.5"
51
- onSubmit={(e) => {
52
- if (loading) return;
53
-
54
- e.preventDefault();
55
- setLoading(true);
56
- generate(prompt).then((res) => {
57
- console.log(res);
58
- if (!res) return;
59
- setRunId(res.run_id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  });
61
- setStatus("preparing");
62
- }}
63
- >
64
- <Label htmlFor="picture">Image prompt</Label>
65
- <Input
66
- id="picture"
67
- type="text"
68
- value={prompt}
69
- onChange={(e) => setPrompt(e.target.value)}
70
- />
71
- <Button type="submit" className="flex gap-2" disabled={loading}>
72
- Generate {loading && <LoadingIcon />}
73
- </Button>
74
-
75
- <div className="border border-gray-200 w-full square h-[400px] rounded-lg relative">
76
- {!loading && image && (
77
- <img
78
- className="w-full h-full object-contain"
79
- src={image}
80
- alt="Generated image"
81
- ></img>
82
- )}
83
- {loading && (
84
- <div className="absolute top-0 left-0 w-full h-full flex items-center justify-center gap-2">
85
- {status} <LoadingIcon />
86
- </div>
87
- )}
88
- {loading && <Skeleton className="w-full h-full" />}
89
- </div>
90
- </form>
91
- </CardContent>
92
- </Card>
93
- </main>
94
  );
95
  }
 
6
  import { Input } from "@/components/ui/input";
7
  import { Label } from "@/components/ui/label";
8
  import { Skeleton } from "@/components/ui/skeleton";
9
+ 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-[500px]">
23
+ <TabsList className="grid w-full grid-cols-2">
24
+ <TabsTrigger value="txt2img">txt2img</TabsTrigger>
25
+ <TabsTrigger value="img2img">img2img</TabsTrigger>
26
+ </TabsList>
27
+ <TabsContent value="txt2img">
28
+ <Txt2img />
29
+ </TabsContent>
30
+ <TabsContent value="img2img">
31
+ <Img2img />
32
+ </TabsContent>
33
+ </Tabs>
34
+ </main>
35
+ );
36
+ }
37
+
38
+ export function Txt2img() {
39
  const [prompt, setPrompt] = useState("");
40
  const [image, setImage] = useState("");
41
  const [loading, setLoading] = useState(false);
42
  const [runId, setRunId] = useState("");
43
+ const [status, setStatus] = useState<string>();
44
 
45
  // Polling in frontend to check for the
46
  useEffect(() => {
 
49
  checkStatus(runId).then((res) => {
50
  if (res) setStatus(res.status);
51
  if (res && res.status === "success") {
52
+ console.log(res.outputs[0]?.data);
53
+ setImage(res.outputs[0]?.data?.images[0].url);
54
  setLoading(false);
55
  clearInterval(interval);
56
  }
 
60
  }, [runId]);
61
 
62
  return (
63
+ <Card className="w-full max-w-[500px]">
64
+ <CardHeader>
65
+ Comfy Deploy - Vector Line Art Tool
66
+ <div className="text-xs text-foreground opacity-50">
67
+ Lora -{" "}
68
+ <a href="https://civitai.com/models/256144/stick-line-vector-illustration">
69
+ stick-line-vector-illustration
70
+ </a>
71
+ </div>
72
+ </CardHeader>
73
+ <CardContent>
74
+ <form
75
+ className="grid w-full items-center gap-1.5"
76
+ onSubmit={(e) => {
77
+ if (loading) return;
78
+
79
+ e.preventDefault();
80
+ setLoading(true);
81
+ generate(prompt).then((res) => {
82
+ console.log(res);
83
+ if (!res) {
84
+ setStatus("error");
85
+ setLoading(false);
86
+ return;
87
+ }
88
+ setRunId(res.run_id);
89
+ });
90
+ setStatus("preparing");
91
+ }}
92
+ >
93
+ <Label htmlFor="picture">Image prompt</Label>
94
+ <Input
95
+ id="picture"
96
+ type="text"
97
+ value={prompt}
98
+ onChange={(e) => setPrompt(e.target.value)}
99
+ />
100
+ <Button type="submit" className="flex gap-2" disabled={loading}>
101
+ Generate {loading && <LoadingIcon />}
102
+ </Button>
103
+
104
+ <div className="border border-gray-200 w-full square h-[400px] rounded-lg relative">
105
+ {!loading && image && (
106
+ <img
107
+ className="w-full h-full object-contain"
108
+ src={image}
109
+ alt="Generated image"
110
+ ></img>
111
+ )}
112
+ {!image && status && (
113
+ <div className="absolute top-0 left-0 w-full h-full flex items-center justify-center gap-2">
114
+ {status} <LoadingIcon />
115
+ </div>
116
+ )}
117
+ {loading && <Skeleton className="w-full h-full" />}
118
  </div>
119
+ </form>
120
+ </CardContent>
121
+ </Card>
122
+ );
123
+ }
124
+
125
+ export function Img2img() {
126
+ const [prompt, setPrompt] = useState<File>();
127
+ const [image, setImage] = useState("");
128
+ const [loading, setLoading] = useState(false);
129
+ const [runId, setRunId] = useState("");
130
+ const [status, setStatus] = useState<string>();
131
+
132
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
133
+ setPrompt(e.target.files[0]);
134
+ };
135
+
136
+ // Polling in frontend to check for the
137
+ useEffect(() => {
138
+ if (!runId) return;
139
+ const interval = setInterval(() => {
140
+ checkStatus(runId).then((res) => {
141
+ if (res) setStatus(res.status);
142
+ if (res && res.status === "success") {
143
+ console.log(res.outputs[0]?.data);
144
+ setImage(res.outputs[0]?.data?.images[0].url);
145
+ setLoading(false);
146
+ clearInterval(interval);
147
+ }
148
+ });
149
+ }, 2000);
150
+ return () => clearInterval(interval);
151
+ }, [runId]);
152
+
153
+ return (
154
+ <Card className="w-full max-w-[500px]">
155
+ <CardHeader>
156
+ Comfy Deploy - Vector Line Art Tool
157
+ <div className="text-xs text-foreground opacity-50">
158
+ Lora -{" "}
159
+ <a href="https://civitai.com/models/256144/stick-line-vector-illustration">
160
+ stick-line-vector-illustration
161
+ </a>
162
+ </div>
163
+ </CardHeader>
164
+ <CardContent>
165
+ <form
166
+ className="grid w-full items-center gap-1.5"
167
+ onSubmit={(e) => {
168
+ e.preventDefault()
169
+ if (loading) return;
170
+ if (!prompt) return;
171
+
172
+ setImage("");
173
+
174
+ setStatus("getting url for upload");
175
+
176
+ console.log(prompt?.type, prompt?.size);
177
+
178
+ getUploadUrl(prompt?.type, prompt?.size).then((res) => {
179
+ if (!res) return;
180
+
181
+ setStatus("uploading input");
182
+
183
+ console.log(res);
184
+
185
+ fetch(res.upload_url, {
186
+ method: "PUT",
187
+ body: prompt,
188
+ headers: {
189
+ "Content-Type": prompt.type,
190
+ "x-amz-acl": "public-read",
191
+ "Content-Length": prompt.size.toString(),
192
+ },
193
+ }).then((_res) => {
194
+ if (_res.ok) {
195
+ setStatus("uploaded input");
196
+
197
+ setLoading(true);
198
+ generate_img(res.download_url).then((res) => {
199
+ console.log(res);
200
+ if (!res) {
201
+ setStatus("error");
202
+ setLoading(false);
203
+ return;
204
+ }
205
+ setRunId(res.run_id);
206
+ });
207
+ setStatus("preparing");
208
+ }
209
  });
210
+ });
211
+ }}
212
+ >
213
+ <Label htmlFor="picture">Image prompt</Label>
214
+ <Input id="picture" type="file" onChange={handleFileChange} />
215
+ <Button type="submit" className="flex gap-2" disabled={loading}>
216
+ Generate {loading && <LoadingIcon />}
217
+ </Button>
218
+
219
+ <div className="border border-gray-200 w-full square h-[400px] rounded-lg relative">
220
+ {!loading && image && (
221
+ <img
222
+ className="w-full h-full object-contain"
223
+ src={image}
224
+ alt="Generated image"
225
+ ></img>
226
+ )}
227
+ {!image && status && (
228
+ <div className="absolute top-0 left-0 w-full h-full flex items-center justify-center gap-2">
229
+ {status} <LoadingIcon />
230
+ </div>
231
+ )}
232
+ {loading && <Skeleton className="w-full h-full" />}
233
+ </div>
234
+ </form>
235
+ </CardContent>
236
+ </Card>
 
 
 
 
 
 
237
  );
238
  }
src/components/ui/tabs.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as TabsPrimitive from "@radix-ui/react-tabs"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Tabs = TabsPrimitive.Root
9
+
10
+ const TabsList = React.forwardRef<
11
+ React.ElementRef<typeof TabsPrimitive.List>,
12
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
13
+ >(({ className, ...props }, ref) => (
14
+ <TabsPrimitive.List
15
+ ref={ref}
16
+ className={cn(
17
+ "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ ))
23
+ TabsList.displayName = TabsPrimitive.List.displayName
24
+
25
+ const TabsTrigger = React.forwardRef<
26
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
27
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
28
+ >(({ className, ...props }, ref) => (
29
+ <TabsPrimitive.Trigger
30
+ ref={ref}
31
+ className={cn(
32
+ "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
33
+ className
34
+ )}
35
+ {...props}
36
+ />
37
+ ))
38
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39
+
40
+ const TabsContent = React.forwardRef<
41
+ React.ElementRef<typeof TabsPrimitive.Content>,
42
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
43
+ >(({ className, ...props }, ref) => (
44
+ <TabsPrimitive.Content
45
+ ref={ref}
46
+ className={cn(
47
+ "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ ))
53
+ TabsContent.displayName = TabsPrimitive.Content.displayName
54
+
55
+ export { Tabs, TabsList, TabsTrigger, TabsContent }
src/lib/comfy-deploy.ts CHANGED
@@ -14,6 +14,12 @@ const runOutputTypes = z.object({
14
  ),
15
  });
16
 
 
 
 
 
 
 
17
  export class ComfyDeployClient {
18
  apiBase: string = "https://www.comfydeploy.com/api";
19
  apiToken: string;
@@ -96,4 +102,27 @@ export class ComfyDeployClient {
96
 
97
  return run;
98
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  }
 
14
  ),
15
  });
16
 
17
+ 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;
 
102
 
103
  return run;
104
  }
105
+
106
+ async getUploadUrl(type: string, file_size: number) {
107
+ const obj = {
108
+ type: type,
109
+ file_size: file_size.toString(),
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))
123
+ .catch((err) => {
124
+ console.error(err);
125
+ return null;
126
+ });
127
+ }
128
  }
src/server/generate.tsx CHANGED
@@ -3,6 +3,7 @@
3
  import { ComfyDeployClient } from "@/lib/comfy-deploy"
4
 
5
  const client = new ComfyDeployClient({
 
6
  apiToken: process.env.COMFY_API_TOKEN!,
7
  })
8
 
@@ -15,6 +16,23 @@ export async function generate(prompt: string){
15
  })
16
  }
17
 
 
 
 
 
 
 
 
 
 
18
  export async function checkStatus(run_id: string){
19
  return await client.getRun(run_id)
 
 
 
 
 
 
 
 
20
  }
 
3
  import { ComfyDeployClient } from "@/lib/comfy-deploy"
4
 
5
  const client = new ComfyDeployClient({
6
+ apiBase: process.env.COMFY_API_URL,
7
  apiToken: process.env.COMFY_API_TOKEN!,
8
  })
9
 
 
16
  })
17
  }
18
 
19
+ export async function generate_img(input_image: string){
20
+ return await client.run({
21
+ deployment_id: process.env.COMFY_DEPLOYMENT_ID_IMG_2_IMG!,
22
+ inputs: {
23
+ "input_image": input_image
24
+ }
25
+ })
26
+ }
27
+
28
  export async function checkStatus(run_id: string){
29
  return await client.getRun(run_id)
30
+ }
31
+
32
+ export async function getUploadUrl(type: string, file_size: number){
33
+ try {
34
+ return await client.getUploadUrl(type, file_size)
35
+ } catch (error) {
36
+ console.log(error)
37
+ }
38
  }