Julian Bilcke
commited on
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 |
+
})
|