Commit
•
f5d8038
1
Parent(s):
2d323fb
fixed upscaling
Browse files- .env +2 -2
- next.config.js +1 -0
- src/app/engine/presets.ts +17 -22
- src/app/engine/render.ts +60 -4
- src/app/interface/bottom-bar/index.tsx +47 -17
- src/app/interface/panel/index.tsx +36 -26
- src/app/main.tsx +10 -9
- src/app/queries/getStory.ts +2 -1
- src/app/store/index.ts +35 -4
- src/lib/sleep.ts +6 -0
.env
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
-
NEXT_PUBLIC_BASE_URL=https://jbilcke-hf-
|
2 |
# NEXT_PUBLIC_RENDERING_ENGINE_API=https://hysts-zeroscope-v2.hf.space
|
3 |
RENDERING_ENGINE_API=https://jbilcke-hf-videochain-api.hf.space
|
4 |
-
#VC_SECRET_ACCESS_TOKEN=<
|
5 |
#HF_API_TOKEN=<SECRET>
|
6 |
#HF_INFERENCE_ENDPOINT_URL=<SECRET>
|
|
|
1 |
+
NEXT_PUBLIC_BASE_URL=https://jbilcke-hf-ai-comic-factory.hf.space
|
2 |
# NEXT_PUBLIC_RENDERING_ENGINE_API=https://hysts-zeroscope-v2.hf.space
|
3 |
RENDERING_ENGINE_API=https://jbilcke-hf-videochain-api.hf.space
|
4 |
+
#VC_SECRET_ACCESS_TOKEN=<secret>
|
5 |
#HF_API_TOKEN=<SECRET>
|
6 |
#HF_INFERENCE_ENDPOINT_URL=<SECRET>
|
next.config.js
CHANGED
@@ -4,6 +4,7 @@ const nextConfig = {
|
|
4 |
|
5 |
experimental: {
|
6 |
serverActions: true,
|
|
|
7 |
},
|
8 |
}
|
9 |
|
|
|
4 |
|
5 |
experimental: {
|
6 |
serverActions: true,
|
7 |
+
serverActionsBodySizeLimit: '8mb',
|
8 |
},
|
9 |
}
|
10 |
|
src/app/engine/presets.ts
CHANGED
@@ -123,13 +123,12 @@ export const presets: Record<string, Preset> = {
|
|
123 |
font: "actionman",
|
124 |
llmPrompt: "american comic",
|
125 |
imagePrompt: (prompt: string) => [
|
126 |
-
`american comic about ${prompt}`,
|
127 |
-
"single panel",
|
128 |
-
"
|
129 |
-
|
130 |
-
"
|
131 |
-
"
|
132 |
-
"color comicbook",
|
133 |
// "color drawing"
|
134 |
],
|
135 |
negativePrompt: () => [
|
@@ -183,13 +182,12 @@ export const presets: Record<string, Preset> = {
|
|
183 |
font: "actionman",
|
184 |
llmPrompt: "american comic",
|
185 |
imagePrompt: (prompt: string) => [
|
186 |
-
`american comic about ${prompt}`,
|
187 |
-
"single panel",
|
188 |
-
|
189 |
-
"comicbook style",
|
190 |
"1950",
|
191 |
"50s",
|
192 |
-
"color comicbook",
|
193 |
// "color drawing"
|
194 |
],
|
195 |
negativePrompt: () => [
|
@@ -244,15 +242,12 @@ export const presets: Record<string, Preset> = {
|
|
244 |
font: "actionman",
|
245 |
llmPrompt: "new pulp science fiction",
|
246 |
imagePrompt: (prompt: string) => [
|
247 |
-
`color comic panel`,
|
248 |
`${prompt}`,
|
249 |
"40s",
|
250 |
"1940",
|
251 |
-
"vintage comic",
|
252 |
-
"pulp magazine",
|
253 |
-
"pulp science fiction",
|
254 |
"vintage science fiction",
|
255 |
-
"single panel",
|
256 |
// "comic album"
|
257 |
],
|
258 |
negativePrompt: () => [
|
@@ -311,9 +306,9 @@ export const presets: Record<string, Preset> = {
|
|
311 |
"tintin style",
|
312 |
"french comic panel",
|
313 |
"franco-belgian style",
|
314 |
-
|
315 |
-
|
316 |
-
"single panel",
|
317 |
// "comic album"
|
318 |
],
|
319 |
negativePrompt: () => [
|
@@ -343,8 +338,8 @@ export const presets: Record<string, Preset> = {
|
|
343 |
"franco-belgian style",
|
344 |
"bande dessinée",
|
345 |
"single panel",
|
346 |
-
"comical",
|
347 |
-
"comic album",
|
348 |
// "color drawing"
|
349 |
],
|
350 |
negativePrompt: () => [
|
|
|
123 |
font: "actionman",
|
124 |
llmPrompt: "american comic",
|
125 |
imagePrompt: (prompt: string) => [
|
126 |
+
`modern american comic about ${prompt}`,
|
127 |
+
//"single panel",
|
128 |
+
"digital color comicbook style",
|
129 |
+
// "2010s",
|
130 |
+
// "digital print",
|
131 |
+
// "color comicbook",
|
|
|
132 |
// "color drawing"
|
133 |
],
|
134 |
negativePrompt: () => [
|
|
|
182 |
font: "actionman",
|
183 |
llmPrompt: "american comic",
|
184 |
imagePrompt: (prompt: string) => [
|
185 |
+
`vintage american color comic about ${prompt}`,
|
186 |
+
// "single panel",
|
187 |
+
// "comicbook style",
|
|
|
188 |
"1950",
|
189 |
"50s",
|
190 |
+
// "color comicbook",
|
191 |
// "color drawing"
|
192 |
],
|
193 |
negativePrompt: () => [
|
|
|
242 |
font: "actionman",
|
243 |
llmPrompt: "new pulp science fiction",
|
244 |
imagePrompt: (prompt: string) => [
|
245 |
+
`vintage color pulp comic panel`,
|
246 |
`${prompt}`,
|
247 |
"40s",
|
248 |
"1940",
|
|
|
|
|
|
|
249 |
"vintage science fiction",
|
250 |
+
// "single panel",
|
251 |
// "comic album"
|
252 |
],
|
253 |
negativePrompt: () => [
|
|
|
306 |
"tintin style",
|
307 |
"french comic panel",
|
308 |
"franco-belgian style",
|
309 |
+
// "color panel",
|
310 |
+
// "bande dessinée",
|
311 |
+
// "single panel",
|
312 |
// "comic album"
|
313 |
],
|
314 |
negativePrompt: () => [
|
|
|
338 |
"franco-belgian style",
|
339 |
"bande dessinée",
|
340 |
"single panel",
|
341 |
+
// "comical",
|
342 |
+
// "comic album",
|
343 |
// "color drawing"
|
344 |
],
|
345 |
negativePrompt: () => [
|
src/app/engine/render.ts
CHANGED
@@ -19,7 +19,7 @@ export async function newRender({
|
|
19 |
width: number
|
20 |
height: number
|
21 |
}) {
|
22 |
-
console.log(`newRender(${prompt})`)
|
23 |
if (!prompt) {
|
24 |
console.error(`cannot call the rendering API without a prompt, aborting..`)
|
25 |
throw new Error(`cannot call the rendering API without a prompt, aborting..`)
|
@@ -37,7 +37,7 @@ export async function newRender({
|
|
37 |
|
38 |
|
39 |
try {
|
40 |
-
console.log(`calling POST ${apiUrl}/render with prompt: ${prompt}`)
|
41 |
|
42 |
const res = await fetch(`${apiUrl}/render`, {
|
43 |
method: "POST",
|
@@ -99,7 +99,7 @@ export async function getRender(renderId: string) {
|
|
99 |
|
100 |
let defaulResult: RenderedScene = {
|
101 |
renderId: "",
|
102 |
-
status: "
|
103 |
assetUrl: "",
|
104 |
alt: "",
|
105 |
maskUrl: "",
|
@@ -114,7 +114,7 @@ export async function getRender(renderId: string) {
|
|
114 |
headers: {
|
115 |
Accept: "application/json",
|
116 |
"Content-Type": "application/json",
|
117 |
-
|
118 |
},
|
119 |
cache: 'no-store',
|
120 |
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
@@ -134,6 +134,62 @@ export async function getRender(renderId: string) {
|
|
134 |
const response = (await res.json()) as RenderedScene
|
135 |
// console.log("response:", response)
|
136 |
return response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
137 |
} catch (err) {
|
138 |
console.error(err)
|
139 |
// Gorgon.clear(cacheKey)
|
|
|
19 |
width: number
|
20 |
height: number
|
21 |
}) {
|
22 |
+
// console.log(`newRender(${prompt})`)
|
23 |
if (!prompt) {
|
24 |
console.error(`cannot call the rendering API without a prompt, aborting..`)
|
25 |
throw new Error(`cannot call the rendering API without a prompt, aborting..`)
|
|
|
37 |
|
38 |
|
39 |
try {
|
40 |
+
// console.log(`calling POST ${apiUrl}/render with prompt: ${prompt}`)
|
41 |
|
42 |
const res = await fetch(`${apiUrl}/render`, {
|
43 |
method: "POST",
|
|
|
99 |
|
100 |
let defaulResult: RenderedScene = {
|
101 |
renderId: "",
|
102 |
+
status: "pending",
|
103 |
assetUrl: "",
|
104 |
alt: "",
|
105 |
maskUrl: "",
|
|
|
114 |
headers: {
|
115 |
Accept: "application/json",
|
116 |
"Content-Type": "application/json",
|
117 |
+
Authorization: `Bearer ${process.env.VC_SECRET_ACCESS_TOKEN}`,
|
118 |
},
|
119 |
cache: 'no-store',
|
120 |
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
|
|
134 |
const response = (await res.json()) as RenderedScene
|
135 |
// console.log("response:", response)
|
136 |
return response
|
137 |
+
} catch (err) {
|
138 |
+
console.error(err)
|
139 |
+
defaulResult.status = "error"
|
140 |
+
defaulResult.error = `${err}`
|
141 |
+
// Gorgon.clear(cacheKey)
|
142 |
+
return defaulResult
|
143 |
+
}
|
144 |
+
|
145 |
+
// }, cacheDurationInSec * 1000)
|
146 |
+
}
|
147 |
+
|
148 |
+
export async function upscaleImage(image: string): Promise<{
|
149 |
+
assetUrl: string
|
150 |
+
error: string
|
151 |
+
}> {
|
152 |
+
if (!image) {
|
153 |
+
console.error(`cannot call the rendering API without an image, aborting..`)
|
154 |
+
throw new Error(`cannot call the rendering API without an image, aborting..`)
|
155 |
+
}
|
156 |
+
|
157 |
+
let defaulResult = {
|
158 |
+
assetUrl: "",
|
159 |
+
error: "failed to fetch the data",
|
160 |
+
}
|
161 |
+
|
162 |
+
try {
|
163 |
+
// console.log(`calling GET ${apiUrl}/render with renderId: ${renderId}`)
|
164 |
+
const res = await fetch(`${apiUrl}/upscale`, {
|
165 |
+
method: "POST",
|
166 |
+
headers: {
|
167 |
+
Accept: "application/json",
|
168 |
+
"Content-Type": "application/json",
|
169 |
+
Authorization: `Bearer ${process.env.VC_SECRET_ACCESS_TOKEN}`,
|
170 |
+
},
|
171 |
+
cache: 'no-store',
|
172 |
+
body: JSON.stringify({ image, factor: 3 })
|
173 |
+
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
174 |
+
// next: { revalidate: 1 }
|
175 |
+
})
|
176 |
+
|
177 |
+
// console.log("res:", res)
|
178 |
+
// The return value is *not* serialized
|
179 |
+
// You can return Date, Map, Set, etc.
|
180 |
+
|
181 |
+
// Recommendation: handle errors
|
182 |
+
if (res.status !== 200) {
|
183 |
+
// This will activate the closest `error.js` Error Boundary
|
184 |
+
throw new Error('Failed to fetch data')
|
185 |
+
}
|
186 |
+
|
187 |
+
const response = (await res.json()) as {
|
188 |
+
assetUrl: string
|
189 |
+
error: string
|
190 |
+
}
|
191 |
+
// console.log("response:", response)
|
192 |
+
return response
|
193 |
} catch (err) {
|
194 |
console.error(err)
|
195 |
// Gorgon.clear(cacheKey)
|
src/app/interface/bottom-bar/index.tsx
CHANGED
@@ -5,6 +5,9 @@ import { base64ToFile } from "@/lib/base64ToFile"
|
|
5 |
import { uploadToHuggingFace } from "@/lib/uploadToHuggingFace"
|
6 |
import { cn } from "@/lib/utils"
|
7 |
import { About } from "../about"
|
|
|
|
|
|
|
8 |
|
9 |
export function BottomBar() {
|
10 |
const download = useStore(state => state.download)
|
@@ -17,15 +20,42 @@ export function BottomBar() {
|
|
17 |
|
18 |
const allStatus = Object.values(panelGenerationStatus)
|
19 |
const remainingImages = allStatus.reduce((acc, s) => (acc + (s ? 1 : 0)), 0)
|
20 |
-
|
21 |
-
|
|
|
|
|
|
|
|
|
|
|
22 |
const handleUpscale = () => {
|
|
|
23 |
startTransition(() => {
|
24 |
-
|
25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
})
|
27 |
}
|
28 |
-
*/
|
29 |
|
30 |
const handleShare = async () => {
|
31 |
const dataUrl = await pageToImage()
|
@@ -83,16 +113,6 @@ ${uploadUrl
|
|
83 |
)}>
|
84 |
<About />
|
85 |
</div>
|
86 |
-
{/*
|
87 |
-
<div>
|
88 |
-
<Button
|
89 |
-
onClick={handleUpscale}
|
90 |
-
disabled={!prompt?.length && remainingImages}
|
91 |
-
>
|
92 |
-
Upscale
|
93 |
-
</Button>
|
94 |
-
</div>
|
95 |
-
*/}
|
96 |
<div className={cn(
|
97 |
`flex flex-row`,
|
98 |
`animation-all duration-300 ease-in-out`,
|
@@ -100,6 +120,16 @@ ${uploadUrl
|
|
100 |
`space-x-3`,
|
101 |
`scale-[0.9]`
|
102 |
)}>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
103 |
<div>
|
104 |
<Button
|
105 |
onClick={handlePrint}
|
@@ -114,10 +144,10 @@ ${uploadUrl
|
|
114 |
disabled={!prompt?.length}
|
115 |
>
|
116 |
<span className="hidden md:inline">{
|
117 |
-
remainingImages ? `${allStatus.length - remainingImages}
|
118 |
}</span>
|
119 |
<span className="inline md:hidden">{
|
120 |
-
remainingImages ? `${allStatus.length - remainingImages}
|
121 |
}</span>
|
122 |
</Button>
|
123 |
</div>
|
|
|
5 |
import { uploadToHuggingFace } from "@/lib/uploadToHuggingFace"
|
6 |
import { cn } from "@/lib/utils"
|
7 |
import { About } from "../about"
|
8 |
+
import { startTransition, useState } from "react"
|
9 |
+
import { upscaleImage } from "@/app/engine/render"
|
10 |
+
import { sleep } from "@/lib/sleep"
|
11 |
|
12 |
export function BottomBar() {
|
13 |
const download = useStore(state => state.download)
|
|
|
20 |
|
21 |
const allStatus = Object.values(panelGenerationStatus)
|
22 |
const remainingImages = allStatus.reduce((acc, s) => (acc + (s ? 1 : 0)), 0)
|
23 |
+
|
24 |
+
const upscaleQueue = useStore(state => state.upscaleQueue)
|
25 |
+
const renderedScenes = useStore(state => state.renderedScenes)
|
26 |
+
const removeFromUpscaleQueue = useStore(state => state.removeFromUpscaleQueue)
|
27 |
+
const setRendered = useStore(state => state.setRendered)
|
28 |
+
const [isUpscaling, setUpscaling] = useState(false)
|
29 |
+
|
30 |
const handleUpscale = () => {
|
31 |
+
setUpscaling(true)
|
32 |
startTransition(() => {
|
33 |
+
const fn = async () => {
|
34 |
+
for (let [panelId, renderedScene] of Object.entries(upscaleQueue)) {
|
35 |
+
try {
|
36 |
+
console.log(`upscaling panel ${panelId} (${renderedScene.renderId})`)
|
37 |
+
const result = await upscaleImage(renderedScene.assetUrl)
|
38 |
+
await sleep(1000)
|
39 |
+
if (result.assetUrl) {
|
40 |
+
console.log(`upscale successful, removing ${panelId} (${renderedScene.renderId}) from upscale queue`)
|
41 |
+
setRendered(panelId, {
|
42 |
+
...renderedScene,
|
43 |
+
assetUrl: result.assetUrl
|
44 |
+
})
|
45 |
+
removeFromUpscaleQueue(panelId)
|
46 |
+
}
|
47 |
+
|
48 |
+
} catch (err) {
|
49 |
+
console.error(`failed to upscale: ${err}`)
|
50 |
+
}
|
51 |
+
}
|
52 |
+
|
53 |
+
setUpscaling(false)
|
54 |
+
}
|
55 |
+
|
56 |
+
fn()
|
57 |
})
|
58 |
}
|
|
|
59 |
|
60 |
const handleShare = async () => {
|
61 |
const dataUrl = await pageToImage()
|
|
|
113 |
)}>
|
114 |
<About />
|
115 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
116 |
<div className={cn(
|
117 |
`flex flex-row`,
|
118 |
`animation-all duration-300 ease-in-out`,
|
|
|
120 |
`space-x-3`,
|
121 |
`scale-[0.9]`
|
122 |
)}>
|
123 |
+
<div>
|
124 |
+
<Button
|
125 |
+
onClick={handleUpscale}
|
126 |
+
disabled={!prompt?.length || remainingImages > 0 || !Object.values(upscaleQueue).length}
|
127 |
+
>
|
128 |
+
{isUpscaling
|
129 |
+
? `${allStatus.length - Object.values(upscaleQueue).length}/${allStatus.length} ⌛`
|
130 |
+
: "Upscale"}
|
131 |
+
</Button>
|
132 |
+
</div>
|
133 |
<div>
|
134 |
<Button
|
135 |
onClick={handlePrint}
|
|
|
144 |
disabled={!prompt?.length}
|
145 |
>
|
146 |
<span className="hidden md:inline">{
|
147 |
+
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} panels ⌛` : `Save`
|
148 |
}</span>
|
149 |
<span className="inline md:hidden">{
|
150 |
+
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} ⌛` : `Save`
|
151 |
}</span>
|
152 |
</Button>
|
153 |
</div>
|
src/app/interface/panel/index.tsx
CHANGED
@@ -26,9 +26,12 @@ export function Panel({
|
|
26 |
width?: number
|
27 |
height?: number
|
28 |
}) {
|
|
|
|
|
29 |
const ref = useRef<HTMLImageElement>(null)
|
30 |
const font = useStore(state => state.font)
|
31 |
const preset = useStore(state => state.preset)
|
|
|
32 |
const setGeneratingImages = useStore(state => state.setGeneratingImages)
|
33 |
|
34 |
const [imageWithText, setImageWithText] = useState("")
|
@@ -41,13 +44,18 @@ export function Panel({
|
|
41 |
const zoomLevel = useStore(state => state.zoomLevel)
|
42 |
const showCaptions = useStore(state => state.showCaptions)
|
43 |
|
44 |
-
|
45 |
-
// const captions = useStore(state => state.captions)
|
46 |
-
// const caption = captions[panel] || ""
|
47 |
|
48 |
const [_isPending, startTransition] = useTransition()
|
49 |
-
const
|
|
|
|
|
|
|
|
|
|
|
50 |
const renderedRef = useRef<RenderedScene>()
|
|
|
|
|
51 |
|
52 |
const timeoutRef = useRef<any>(null)
|
53 |
|
@@ -60,41 +68,40 @@ export function Panel({
|
|
60 |
if (!prompt?.length) { return }
|
61 |
|
62 |
// important: update the status, and clear the scene
|
63 |
-
setGeneratingImages(
|
64 |
|
65 |
// just to empty it
|
66 |
-
setRendered(getInitialRenderedScene())
|
67 |
|
68 |
setTimeout(() => {
|
69 |
startTransition(async () => {
|
70 |
|
71 |
-
console.log(`Loading panel ${panel}..`)
|
72 |
|
73 |
let newRendered: RenderedScene
|
74 |
try {
|
75 |
newRendered = await newRender({ prompt, width, height })
|
76 |
} catch (err) {
|
77 |
-
|
78 |
newRendered = await newRender({ prompt, width, height })
|
79 |
}
|
80 |
|
81 |
if (newRendered) {
|
82 |
// console.log("newRendered:", newRendered)
|
83 |
-
setRendered(
|
84 |
-
// addRenderedScene(newRendered)
|
85 |
|
86 |
// but we are still loading!
|
87 |
} else {
|
88 |
-
setRendered(
|
89 |
renderId: "",
|
90 |
-
status: "
|
91 |
assetUrl: "",
|
92 |
alt: "",
|
93 |
maskUrl: "",
|
94 |
-
error: "
|
95 |
segments: []
|
96 |
})
|
97 |
-
setGeneratingImages(
|
98 |
return
|
99 |
}
|
100 |
})
|
@@ -111,15 +118,15 @@ export function Panel({
|
|
111 |
return
|
112 |
}
|
113 |
try {
|
114 |
-
setGeneratingImages(
|
115 |
// console.log(`Checking job status API for job ${renderedRef.current?.renderId}`)
|
116 |
const newRendered = await getRender(renderedRef.current.renderId)
|
117 |
// console.log("got a response!", newRendered)
|
118 |
|
119 |
if (JSON.stringify(renderedRef.current) !== JSON.stringify(newRendered)) {
|
120 |
-
console.log("updated panel:", newRendered)
|
121 |
-
setRendered(renderedRef.current = newRendered)
|
122 |
-
setGeneratingImages(
|
123 |
}
|
124 |
// console.log("status:", newRendered.status)
|
125 |
|
@@ -128,17 +135,18 @@ export function Panel({
|
|
128 |
timeoutRef.current = setTimeout(checkStatus, delay)
|
129 |
} else if (newRendered.status === "error" ||
|
130 |
(newRendered.status === "completed" && !newRendered.assetUrl?.length)) {
|
131 |
-
console.log(`panel got an error and/or an empty asset url :/ "${newRendered.error}", but let's try to recover..`)
|
132 |
try {
|
133 |
const newAttempt = await newRender({ prompt, width, height })
|
134 |
-
setRendered(
|
135 |
} catch (err) {
|
136 |
-
console.error("yeah sorry, something is wrong.. aborting")
|
137 |
-
setGeneratingImages(
|
138 |
}
|
139 |
} else {
|
140 |
console.log("panel finished!")
|
141 |
-
setGeneratingImages(
|
|
|
142 |
}
|
143 |
} catch (err) {
|
144 |
console.error(err)
|
@@ -251,7 +259,6 @@ export function Panel({
|
|
251 |
zoomLevel > 40 ? `border-b-[0.5px] md:border-b-[1px]` :
|
252 |
`border-transparent md:border-b-[0.5px]`,
|
253 |
`print:border-b-[1.5px]`,
|
254 |
-
showCaptions ? `block` : `hidden`,
|
255 |
`truncate`,
|
256 |
|
257 |
zoomLevel > 200 ? `p-4 md:p-8` :
|
@@ -271,8 +278,11 @@ export function Panel({
|
|
271 |
zoomLevel > 120 ? `text-3xs md:text-xl` :
|
272 |
zoomLevel > 100 ? `text-4xs md:text-lg` :
|
273 |
zoomLevel > 90 ? `text-5xs md:text-sm` :
|
274 |
-
zoomLevel > 40 ? `
|
275 |
-
|
|
|
|
|
|
|
276 |
)}
|
277 |
>{caption || ""}
|
278 |
</div>
|
|
|
26 |
width?: number
|
27 |
height?: number
|
28 |
}) {
|
29 |
+
const panelId = `${panel}`
|
30 |
+
|
31 |
const ref = useRef<HTMLImageElement>(null)
|
32 |
const font = useStore(state => state.font)
|
33 |
const preset = useStore(state => state.preset)
|
34 |
+
|
35 |
const setGeneratingImages = useStore(state => state.setGeneratingImages)
|
36 |
|
37 |
const [imageWithText, setImageWithText] = useState("")
|
|
|
44 |
const zoomLevel = useStore(state => state.zoomLevel)
|
45 |
const showCaptions = useStore(state => state.showCaptions)
|
46 |
|
47 |
+
const addToUpscaleQueue = useStore(state => state.addToUpscaleQueue)
|
|
|
|
|
48 |
|
49 |
const [_isPending, startTransition] = useTransition()
|
50 |
+
const renderedScenes = useStore(state => state.renderedScenes)
|
51 |
+
const setRendered = useStore(state => state.setRendered)
|
52 |
+
|
53 |
+
const rendered = renderedScenes[panel] || getInitialRenderedScene()
|
54 |
+
|
55 |
+
// keep a ref in sync
|
56 |
const renderedRef = useRef<RenderedScene>()
|
57 |
+
const renderedKey = JSON.stringify(rendered)
|
58 |
+
useEffect(() => { renderedRef.current = rendered }, [renderedKey])
|
59 |
|
60 |
const timeoutRef = useRef<any>(null)
|
61 |
|
|
|
68 |
if (!prompt?.length) { return }
|
69 |
|
70 |
// important: update the status, and clear the scene
|
71 |
+
setGeneratingImages(panelId, true)
|
72 |
|
73 |
// just to empty it
|
74 |
+
setRendered(panelId, getInitialRenderedScene())
|
75 |
|
76 |
setTimeout(() => {
|
77 |
startTransition(async () => {
|
78 |
|
79 |
+
// console.log(`Loading panel ${panel}..`)
|
80 |
|
81 |
let newRendered: RenderedScene
|
82 |
try {
|
83 |
newRendered = await newRender({ prompt, width, height })
|
84 |
} catch (err) {
|
85 |
+
// "Failed to load the panel! Don't worry, we are retrying..")
|
86 |
newRendered = await newRender({ prompt, width, height })
|
87 |
}
|
88 |
|
89 |
if (newRendered) {
|
90 |
// console.log("newRendered:", newRendered)
|
91 |
+
setRendered(panelId, newRendered)
|
|
|
92 |
|
93 |
// but we are still loading!
|
94 |
} else {
|
95 |
+
setRendered(panelId, {
|
96 |
renderId: "",
|
97 |
+
status: "pending",
|
98 |
assetUrl: "",
|
99 |
alt: "",
|
100 |
maskUrl: "",
|
101 |
+
error: "",
|
102 |
segments: []
|
103 |
})
|
104 |
+
setGeneratingImages(panelId, false)
|
105 |
return
|
106 |
}
|
107 |
})
|
|
|
118 |
return
|
119 |
}
|
120 |
try {
|
121 |
+
setGeneratingImages(panelId, true)
|
122 |
// console.log(`Checking job status API for job ${renderedRef.current?.renderId}`)
|
123 |
const newRendered = await getRender(renderedRef.current.renderId)
|
124 |
// console.log("got a response!", newRendered)
|
125 |
|
126 |
if (JSON.stringify(renderedRef.current) !== JSON.stringify(newRendered)) {
|
127 |
+
// console.log("updated panel:", newRendered)
|
128 |
+
setRendered(panelId, renderedRef.current = newRendered)
|
129 |
+
setGeneratingImages(panelId, true)
|
130 |
}
|
131 |
// console.log("status:", newRendered.status)
|
132 |
|
|
|
135 |
timeoutRef.current = setTimeout(checkStatus, delay)
|
136 |
} else if (newRendered.status === "error" ||
|
137 |
(newRendered.status === "completed" && !newRendered.assetUrl?.length)) {
|
138 |
+
// console.log(`panel got an error and/or an empty asset url :/ "${newRendered.error}", but let's try to recover..`)
|
139 |
try {
|
140 |
const newAttempt = await newRender({ prompt, width, height })
|
141 |
+
setRendered(panelId, newAttempt)
|
142 |
} catch (err) {
|
143 |
+
console.error("yeah sorry, something is wrong.. aborting", err)
|
144 |
+
setGeneratingImages(panelId, false)
|
145 |
}
|
146 |
} else {
|
147 |
console.log("panel finished!")
|
148 |
+
setGeneratingImages(panelId, false)
|
149 |
+
addToUpscaleQueue(panelId, newRendered)
|
150 |
}
|
151 |
} catch (err) {
|
152 |
console.error(err)
|
|
|
259 |
zoomLevel > 40 ? `border-b-[0.5px] md:border-b-[1px]` :
|
260 |
`border-transparent md:border-b-[0.5px]`,
|
261 |
`print:border-b-[1.5px]`,
|
|
|
262 |
`truncate`,
|
263 |
|
264 |
zoomLevel > 200 ? `p-4 md:p-8` :
|
|
|
278 |
zoomLevel > 120 ? `text-3xs md:text-xl` :
|
279 |
zoomLevel > 100 ? `text-4xs md:text-lg` :
|
280 |
zoomLevel > 90 ? `text-5xs md:text-sm` :
|
281 |
+
zoomLevel > 40 ? `md:text-xs` : `md:text-2xs`,
|
282 |
+
|
283 |
+
showCaptions ? (
|
284 |
+
zoomLevel > 90 ? `block` : `hidden md:block`
|
285 |
+
) : `hidden`,
|
286 |
)}
|
287 |
>{caption || ""}
|
288 |
</div>
|
src/app/main.tsx
CHANGED
@@ -30,7 +30,6 @@ export default function Main() {
|
|
30 |
|
31 |
const [waitABitMore, setWaitABitMore] = useState(false)
|
32 |
|
33 |
-
|
34 |
// react to prompt changes
|
35 |
useEffect(() => {
|
36 |
if (!prompt) { return }
|
@@ -42,27 +41,29 @@ export default function Main() {
|
|
42 |
try {
|
43 |
|
44 |
const llmResponse = await getStory({ preset, prompt })
|
45 |
-
console.log("
|
46 |
|
47 |
// we have to limit the size of the prompt, otherwise the rest of the style won't be followed
|
48 |
|
49 |
let limitedPrompt = prompt.slice(0, 77)
|
50 |
-
|
|
|
|
|
51 |
|
52 |
const panelPromptPrefix = preset.imagePrompt(limitedPrompt).join(", ")
|
53 |
-
|
54 |
-
|
55 |
const nbPanels = 4
|
56 |
const newPanels: string[] = []
|
57 |
const newCaptions: string[] = []
|
58 |
setWaitABitMore(true)
|
59 |
-
|
60 |
for (let p = 0; p < nbPanels; p++) {
|
61 |
newCaptions.push(llmResponse[p]?.caption || "...")
|
62 |
-
const newPanel = [panelPromptPrefix, llmResponse[p]?.instructions || ""]
|
63 |
-
newPanels.push(newPanel
|
|
|
64 |
}
|
65 |
-
|
66 |
setCaptions(newCaptions)
|
67 |
setPanels(newPanels)
|
68 |
} catch (err) {
|
|
|
30 |
|
31 |
const [waitABitMore, setWaitABitMore] = useState(false)
|
32 |
|
|
|
33 |
// react to prompt changes
|
34 |
useEffect(() => {
|
35 |
if (!prompt) { return }
|
|
|
41 |
try {
|
42 |
|
43 |
const llmResponse = await getStory({ preset, prompt })
|
44 |
+
console.log("LLM responded:", llmResponse)
|
45 |
|
46 |
// we have to limit the size of the prompt, otherwise the rest of the style won't be followed
|
47 |
|
48 |
let limitedPrompt = prompt.slice(0, 77)
|
49 |
+
if (limitedPrompt.length !== prompt.length) {
|
50 |
+
console.log("Sorry folks, the prompt was cut to:", limitedPrompt)
|
51 |
+
}
|
52 |
|
53 |
const panelPromptPrefix = preset.imagePrompt(limitedPrompt).join(", ")
|
54 |
+
|
|
|
55 |
const nbPanels = 4
|
56 |
const newPanels: string[] = []
|
57 |
const newCaptions: string[] = []
|
58 |
setWaitABitMore(true)
|
59 |
+
console.log("Panel prompts for SDXL:")
|
60 |
for (let p = 0; p < nbPanels; p++) {
|
61 |
newCaptions.push(llmResponse[p]?.caption || "...")
|
62 |
+
const newPanel = [panelPromptPrefix, llmResponse[p]?.instructions || ""].map(chunk => chunk).join(", ")
|
63 |
+
newPanels.push(newPanel)
|
64 |
+
console.log(newPanel)
|
65 |
}
|
66 |
+
|
67 |
setCaptions(newCaptions)
|
68 |
setPanels(newPanels)
|
69 |
} catch (err) {
|
src/app/queries/getStory.ts
CHANGED
@@ -54,7 +54,7 @@ export const getStory = async ({
|
|
54 |
}
|
55 |
}
|
56 |
|
57 |
-
console.log("Raw response from LLM:", result)
|
58 |
const tmp = cleanJson(result)
|
59 |
|
60 |
let llmResponse: LLMResponse = []
|
@@ -63,6 +63,7 @@ export const getStory = async ({
|
|
63 |
llmResponse = dirtyLLMJsonParser(tmp)
|
64 |
} catch (err) {
|
65 |
console.log(`failed to read LLM response: ${err}`)
|
|
|
66 |
|
67 |
// in case of failure here, it might be because the LLM hallucinated a completely different response,
|
68 |
// such as markdown. There is no real solution.. but we can try a fallback:
|
|
|
54 |
}
|
55 |
}
|
56 |
|
57 |
+
// console.log("Raw response from LLM:", result)
|
58 |
const tmp = cleanJson(result)
|
59 |
|
60 |
let llmResponse: LLMResponse = []
|
|
|
63 |
llmResponse = dirtyLLMJsonParser(tmp)
|
64 |
} catch (err) {
|
65 |
console.log(`failed to read LLM response: ${err}`)
|
66 |
+
console.log(`original response was:`, result)
|
67 |
|
68 |
// in case of failure here, it might be because the LLM hallucinated a completely different response,
|
69 |
// such as markdown. There is no real solution.. but we can try a fallback:
|
src/app/store/index.ts
CHANGED
@@ -15,7 +15,9 @@ export const useStore = create<{
|
|
15 |
nbFrames: number
|
16 |
panels: string[]
|
17 |
captions: string[]
|
|
|
18 |
showCaptions: boolean
|
|
|
19 |
layout: LayoutName
|
20 |
layouts: LayoutName[]
|
21 |
zoomLevel: number
|
@@ -24,6 +26,9 @@ export const useStore = create<{
|
|
24 |
panelGenerationStatus: Record<number, boolean>
|
25 |
isGeneratingText: boolean
|
26 |
atLeastOnePanelIsBusy: boolean
|
|
|
|
|
|
|
27 |
setPrompt: (prompt: string) => void
|
28 |
setFont: (font: FontName) => void
|
29 |
setPreset: (preset: Preset) => void
|
@@ -35,7 +40,7 @@ export const useStore = create<{
|
|
35 |
setZoomLevel: (zoomLevel: number) => void
|
36 |
setPage: (page: HTMLDivElement) => void
|
37 |
setGeneratingStory: (isGeneratingStory: boolean) => void
|
38 |
-
setGeneratingImages: (panelId:
|
39 |
setGeneratingText: (isGeneratingText: boolean) => void
|
40 |
pageToImage: () => Promise<string>
|
41 |
download: () => Promise<void>
|
@@ -47,6 +52,8 @@ export const useStore = create<{
|
|
47 |
nbFrames: 1,
|
48 |
panels: [],
|
49 |
captions: [],
|
|
|
|
|
50 |
showCaptions: false,
|
51 |
layout: defaultLayout,
|
52 |
layouts: [defaultLayout, defaultLayout],
|
@@ -56,6 +63,31 @@ export const useStore = create<{
|
|
56 |
panelGenerationStatus: {},
|
57 |
isGeneratingText: false,
|
58 |
atLeastOnePanelIsBusy: false,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
setPrompt: (prompt: string) => {
|
60 |
const existingPrompt = get().prompt
|
61 |
if (prompt === existingPrompt) { return }
|
@@ -112,9 +144,8 @@ export const useStore = create<{
|
|
112 |
set({ page })
|
113 |
},
|
114 |
setGeneratingStory: (isGeneratingStory: boolean) => set({ isGeneratingStory }),
|
115 |
-
setGeneratingImages: (panelId:
|
116 |
-
|
117 |
-
const panelGenerationStatus: Record<number, boolean> = {
|
118 |
...get().panelGenerationStatus,
|
119 |
[panelId]: value
|
120 |
}
|
|
|
15 |
nbFrames: number
|
16 |
panels: string[]
|
17 |
captions: string[]
|
18 |
+
upscaleQueue: Record<string, RenderedScene>
|
19 |
showCaptions: boolean
|
20 |
+
renderedScenes: Record<string, RenderedScene>
|
21 |
layout: LayoutName
|
22 |
layouts: LayoutName[]
|
23 |
zoomLevel: number
|
|
|
26 |
panelGenerationStatus: Record<number, boolean>
|
27 |
isGeneratingText: boolean
|
28 |
atLeastOnePanelIsBusy: boolean
|
29 |
+
setRendered: (panelId: string, renderedScene: RenderedScene) => void
|
30 |
+
addToUpscaleQueue: (panelId: string, renderedScene: RenderedScene) => void
|
31 |
+
removeFromUpscaleQueue: (panelId: string) => void
|
32 |
setPrompt: (prompt: string) => void
|
33 |
setFont: (font: FontName) => void
|
34 |
setPreset: (preset: Preset) => void
|
|
|
40 |
setZoomLevel: (zoomLevel: number) => void
|
41 |
setPage: (page: HTMLDivElement) => void
|
42 |
setGeneratingStory: (isGeneratingStory: boolean) => void
|
43 |
+
setGeneratingImages: (panelId: string, value: boolean) => void
|
44 |
setGeneratingText: (isGeneratingText: boolean) => void
|
45 |
pageToImage: () => Promise<string>
|
46 |
download: () => Promise<void>
|
|
|
52 |
nbFrames: 1,
|
53 |
panels: [],
|
54 |
captions: [],
|
55 |
+
upscaleQueue: {} as Record<string, RenderedScene>,
|
56 |
+
renderedScenes: {} as Record<string, RenderedScene>,
|
57 |
showCaptions: false,
|
58 |
layout: defaultLayout,
|
59 |
layouts: [defaultLayout, defaultLayout],
|
|
|
63 |
panelGenerationStatus: {},
|
64 |
isGeneratingText: false,
|
65 |
atLeastOnePanelIsBusy: false,
|
66 |
+
setRendered: (panelId: string, renderedScene: RenderedScene) => {
|
67 |
+
const { renderedScenes } = get()
|
68 |
+
set({
|
69 |
+
renderedScenes: {
|
70 |
+
...renderedScenes,
|
71 |
+
[panelId]: renderedScene
|
72 |
+
}
|
73 |
+
})
|
74 |
+
},
|
75 |
+
addToUpscaleQueue: (panelId: string, renderedScene: RenderedScene) => {
|
76 |
+
const { upscaleQueue } = get()
|
77 |
+
set({
|
78 |
+
upscaleQueue: {
|
79 |
+
...upscaleQueue,
|
80 |
+
[panelId]: renderedScene
|
81 |
+
},
|
82 |
+
})
|
83 |
+
},
|
84 |
+
removeFromUpscaleQueue: (panelId: string) => {
|
85 |
+
const upscaleQueue = { ...get().upscaleQueue }
|
86 |
+
delete upscaleQueue[panelId]
|
87 |
+
set({
|
88 |
+
upscaleQueue,
|
89 |
+
})
|
90 |
+
},
|
91 |
setPrompt: (prompt: string) => {
|
92 |
const existingPrompt = get().prompt
|
93 |
if (prompt === existingPrompt) { return }
|
|
|
144 |
set({ page })
|
145 |
},
|
146 |
setGeneratingStory: (isGeneratingStory: boolean) => set({ isGeneratingStory }),
|
147 |
+
setGeneratingImages: (panelId: string, value: boolean) => {
|
148 |
+
const panelGenerationStatus: Record<string, boolean> = {
|
|
|
149 |
...get().panelGenerationStatus,
|
150 |
[panelId]: value
|
151 |
}
|
src/lib/sleep.ts
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const sleep = async (durationInMs: number) =>
|
2 |
+
new Promise((resolve) => {
|
3 |
+
setTimeout(() => {
|
4 |
+
resolve(true)
|
5 |
+
}, durationInMs)
|
6 |
+
})
|