Spaces:
Runtime error
Runtime error
Commit
•
2f9a87c
1
Parent(s):
1c1e6e9
the number of pages can now be controlled by the
Browse files- .env +1 -1
- package.json +1 -1
- src/app/interface/about/index.tsx +25 -19
- src/app/interface/bottom-bar/bottom-bar.tsx +6 -3
- src/app/interface/page/index.tsx +22 -26
- src/app/interface/panel/index.tsx +17 -3
- src/app/interface/settings-dialog/index.tsx +23 -21
- src/app/interface/sign-up-cta/sign-up-cta.tsx +1 -1
- src/app/interface/top-menu/index.tsx +4 -1
- src/app/main.tsx +113 -37
- src/app/queries/getStoryContinuation.ts +6 -4
- src/app/queries/predictNextPanels.ts +4 -4
- src/app/store/index.ts +132 -37
.env
CHANGED
@@ -14,7 +14,7 @@ RENDERING_ENGINE="INFERENCE_API"
|
|
14 |
LLM_ENGINE="INFERENCE_API"
|
15 |
|
16 |
# set this to control the number of pages
|
17 |
-
MAX_NB_PAGES=
|
18 |
|
19 |
# Set to "true" to create artificial delays and smooth out traffic
|
20 |
NEXT_PUBLIC_ENABLE_RATE_LIMITER="false"
|
|
|
14 |
LLM_ENGINE="INFERENCE_API"
|
15 |
|
16 |
# set this to control the number of pages
|
17 |
+
MAX_NB_PAGES=2
|
18 |
|
19 |
# Set to "true" to create artificial delays and smooth out traffic
|
20 |
NEXT_PUBLIC_ENABLE_RATE_LIMITER="false"
|
package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
{
|
2 |
"name": "@jbilcke/comic-factory",
|
3 |
-
"version": "
|
4 |
"private": true,
|
5 |
"scripts": {
|
6 |
"dev": "next dev",
|
|
|
1 |
{
|
2 |
"name": "@jbilcke/comic-factory",
|
3 |
+
"version": "1.1.0",
|
4 |
"private": true,
|
5 |
"scripts": {
|
6 |
"dev": "next dev",
|
src/app/interface/about/index.tsx
CHANGED
@@ -1,8 +1,14 @@
|
|
|
|
|
|
1 |
import { Button } from "@/components/ui/button"
|
2 |
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
3 |
-
|
4 |
import { Login } from "../login"
|
5 |
|
|
|
|
|
|
|
|
|
6 |
export function About() {
|
7 |
const [isOpen, setOpen] = useState(false)
|
8 |
|
@@ -10,34 +16,34 @@ export function About() {
|
|
10 |
<Dialog open={isOpen} onOpenChange={setOpen}>
|
11 |
<DialogTrigger asChild>
|
12 |
<Button variant="outline">
|
13 |
-
<span className="hidden md:inline">
|
14 |
-
<span className="inline md:hidden">Version
|
15 |
</Button>
|
16 |
</DialogTrigger>
|
17 |
-
<DialogContent className="sm:max-w-[
|
18 |
<DialogHeader>
|
19 |
-
<DialogTitle>
|
20 |
<DialogDescription className="w-full text-center text-2xl font-bold text-stone-700">
|
21 |
-
|
22 |
</DialogDescription>
|
23 |
</DialogHeader>
|
24 |
<div className="grid gap-4 py-4 text-stone-700 text-sm md:text-base xl:text-lg">
|
25 |
<p className="">
|
26 |
-
The
|
27 |
</p>
|
28 |
<p>
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
This is an open-source project, see the <a className="text-stone-600 underline" href="https://huggingface.co/spaces/jbilcke-hf/ai-comic-factory/blob/main/README.md" target="_blank">README</a> for more info.
|
42 |
</p>
|
43 |
</div>
|
|
|
1 |
+
import { useState } from "react"
|
2 |
+
|
3 |
import { Button } from "@/components/ui/button"
|
4 |
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
5 |
+
|
6 |
import { Login } from "../login"
|
7 |
|
8 |
+
const APP_NAME = `AI Comic Factory`
|
9 |
+
const APP_VERSION = `1.1`
|
10 |
+
const APP_RELEASE_DATE = `March 2024`
|
11 |
+
|
12 |
export function About() {
|
13 |
const [isOpen, setOpen] = useState(false)
|
14 |
|
|
|
16 |
<Dialog open={isOpen} onOpenChange={setOpen}>
|
17 |
<DialogTrigger asChild>
|
18 |
<Button variant="outline">
|
19 |
+
<span className="hidden md:inline">{APP_NAME.replaceAll(" ", "-")} {APP_VERSION}</span>
|
20 |
+
<span className="inline md:hidden">Version {APP_VERSION}</span>
|
21 |
</Button>
|
22 |
</DialogTrigger>
|
23 |
+
<DialogContent className="w-full sm:max-w-[500px] md:max-w-[600px] overflow-y-scroll h-[100vh] sm:h-[550px]">
|
24 |
<DialogHeader>
|
25 |
+
<DialogTitle>{APP_NAME} {APP_VERSION}</DialogTitle>
|
26 |
<DialogDescription className="w-full text-center text-2xl font-bold text-stone-700">
|
27 |
+
{APP_NAME} {APP_VERSION} ({APP_RELEASE_DATE})
|
28 |
</DialogDescription>
|
29 |
</DialogHeader>
|
30 |
<div className="grid gap-4 py-4 text-stone-700 text-sm md:text-base xl:text-lg">
|
31 |
<p className="">
|
32 |
+
The {APP_NAME} generates stories using AI in a few clicks.
|
33 |
</p>
|
34 |
<p>
|
35 |
+
The app is free for Hugging Face users 👉 <Login />
|
36 |
+
</p>
|
37 |
+
<p className="pt-2 pb-2">
|
38 |
+
Are you an artist? Learn <a className="text-stone-600 underline" href="https://huggingface.co/spaces/jbilcke-hf/ai-comic-factory/discussions/402#654ab848fa25dfb780aa19fb" target="_blank">how to use your own art style</a>
|
39 |
+
</p>
|
40 |
+
<p>
|
41 |
+
👉 Default AI model used for stories is <a className="text-stone-600 underline" href="https://huggingface.co/HuggingFaceH4/zephyr-7b-beta" target="_blank">Zephyr-7b-beta</a>
|
42 |
+
</p>
|
43 |
+
<p>
|
44 |
+
👉 Default AI model used for drawing is <a className="text-stone-600 underline" href="https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0" target="_blank">SDXL</a> by Stability AI
|
45 |
+
</p>
|
46 |
+
<p className="pt-2 pb-2">
|
47 |
This is an open-source project, see the <a className="text-stone-600 underline" href="https://huggingface.co/spaces/jbilcke-hf/ai-comic-factory/blob/main/README.md" target="_blank">README</a> for more info.
|
48 |
</p>
|
49 |
</div>
|
src/app/interface/bottom-bar/bottom-bar.tsx
CHANGED
@@ -14,13 +14,16 @@ import { localStorageKeys } from "../settings-dialog/localStorageKeys"
|
|
14 |
import { defaultSettings } from "../settings-dialog/defaultSettings"
|
15 |
|
16 |
function BottomBar() {
|
17 |
-
|
|
|
|
|
|
|
|
|
18 |
const isGeneratingStory = useStore(state => state.isGeneratingStory)
|
19 |
const prompt = useStore(state => state.prompt)
|
20 |
const panelGenerationStatus = useStore(state => state.panelGenerationStatus)
|
21 |
-
|
22 |
const preset = useStore(state => state.preset)
|
23 |
-
const pageToImage = useStore(state => state.pageToImage)
|
24 |
|
25 |
const allStatus = Object.values(panelGenerationStatus)
|
26 |
const remainingImages = allStatus.reduce((acc, s) => (acc + (s ? 1 : 0)), 0)
|
|
|
14 |
import { defaultSettings } from "../settings-dialog/defaultSettings"
|
15 |
|
16 |
function BottomBar() {
|
17 |
+
// deprecated, as HTML-to-bitmap didn't work that well for us
|
18 |
+
// const page = useStore(state => state.page)
|
19 |
+
// const download = useStore(state => state.download)
|
20 |
+
// const pageToImage = useStore(state => state.pageToImage)
|
21 |
+
|
22 |
const isGeneratingStory = useStore(state => state.isGeneratingStory)
|
23 |
const prompt = useStore(state => state.prompt)
|
24 |
const panelGenerationStatus = useStore(state => state.panelGenerationStatus)
|
25 |
+
|
26 |
const preset = useStore(state => state.preset)
|
|
|
27 |
|
28 |
const allStatus = Object.values(panelGenerationStatus)
|
29 |
const remainingImages = allStatus.reduce((acc, s) => (acc + (s ? 1 : 0)), 0)
|
src/app/interface/page/index.tsx
CHANGED
@@ -6,11 +6,13 @@ import { allLayoutAspectRatios, allLayouts } from "@/app/layouts"
|
|
6 |
import { useStore } from "@/app/store"
|
7 |
import { cn } from "@/lib/utils"
|
8 |
|
9 |
-
export function Page({ page }: { page: number}) {
|
10 |
const zoomLevel = useStore(state => state.zoomLevel)
|
11 |
const layouts = useStore(state => state.layouts)
|
12 |
|
13 |
-
|
|
|
|
|
14 |
|
15 |
const LayoutElement = (allLayouts as any)[layout]
|
16 |
const aspectRatio = ((allLayoutAspectRatios as any)[layout] as string) || "aspect-[250/297]"
|
@@ -28,19 +30,16 @@ export function Page({ page }: { page: number}) {
|
|
28 |
// Layout4: currentNbPanelsPerPage
|
29 |
}
|
30 |
|
31 |
-
|
|
|
|
|
32 |
|
|
|
|
|
|
|
|
|
|
|
33 |
/*
|
34 |
-
const [canLoad, setCanLoad] = useState(false)
|
35 |
-
useEffect(() => {
|
36 |
-
if (prompt?.length) {
|
37 |
-
setCanLoad(false)
|
38 |
-
setTimeout(() => {
|
39 |
-
setCanLoad(true)
|
40 |
-
}, page * 4000)
|
41 |
-
}
|
42 |
-
}, [prompt])
|
43 |
-
*/
|
44 |
|
45 |
const setPage = useStore(state => state.setPage)
|
46 |
const pageRef = useRef<HTMLDivElement>(null)
|
@@ -50,18 +49,13 @@ export function Page({ page }: { page: number}) {
|
|
50 |
if (!element) { return }
|
51 |
setPage(element)
|
52 |
}, [pageRef.current])
|
53 |
-
|
54 |
-
/*
|
55 |
-
console.log("PAGE DEBUG:", {
|
56 |
-
currentNbPages,
|
57 |
-
maxNbPages,
|
58 |
-
"currentNbPages < maxNbPages": currentNbPages < maxNbPages,
|
59 |
-
})
|
60 |
*/
|
61 |
-
|
|
|
62 |
return (
|
63 |
<div
|
64 |
-
|
|
|
65 |
className={cn(
|
66 |
`w-full`,
|
67 |
`print:w-screen`,
|
@@ -86,14 +80,16 @@ export function Page({ page }: { page: number}) {
|
|
86 |
// marginLeft: `${zoomLevel > 100 ? `100`}`
|
87 |
}}
|
88 |
>
|
89 |
-
<LayoutElement page={page} nbPanels={
|
90 |
</div>
|
91 |
{currentNbPages > 1 &&
|
92 |
<p className="w-full text-center pt-4 font-sans text-2xs font-semibold text-stone-600">
|
93 |
-
|
94 |
{/*
|
95 |
-
alternative
|
96 |
-
Page {page + 1}
|
|
|
|
|
97 |
*/}
|
98 |
</p>}
|
99 |
</div>
|
|
|
6 |
import { useStore } from "@/app/store"
|
7 |
import { cn } from "@/lib/utils"
|
8 |
|
9 |
+
export function Page({ page }: { page: number }) {
|
10 |
const zoomLevel = useStore(state => state.zoomLevel)
|
11 |
const layouts = useStore(state => state.layouts)
|
12 |
|
13 |
+
// attention: here we use a fallback to layouts[0]
|
14 |
+
// if no predetermined layout exists for this page number
|
15 |
+
const layout = layouts[page] || layouts[0]
|
16 |
|
17 |
const LayoutElement = (allLayouts as any)[layout]
|
18 |
const aspectRatio = ((allLayoutAspectRatios as any)[layout] as string) || "aspect-[250/297]"
|
|
|
30 |
// Layout4: currentNbPanelsPerPage
|
31 |
}
|
32 |
|
33 |
+
// it's a bit confusing and too rigid we can't change the layouts for each panel,
|
34 |
+
// I should refactor this
|
35 |
+
const panelsPerPage = ((allLayoutsNbPanels as any)[layout] as number) || currentNbPanelsPerPage
|
36 |
|
37 |
+
|
38 |
+
// I think we should deprecate this part
|
39 |
+
// this was used to keep track of the page HTML element,
|
40 |
+
// for use with a HTML-to-bitmap library
|
41 |
+
// but the CSS layout wasn't followed properly and it depended on the zoom level
|
42 |
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
|
44 |
const setPage = useStore(state => state.setPage)
|
45 |
const pageRef = useRef<HTMLDivElement>(null)
|
|
|
49 |
if (!element) { return }
|
50 |
setPage(element)
|
51 |
}, [pageRef.current])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
*/
|
53 |
+
|
54 |
+
|
55 |
return (
|
56 |
<div
|
57 |
+
// deprecated
|
58 |
+
// ref={pageRef}
|
59 |
className={cn(
|
60 |
`w-full`,
|
61 |
`print:w-screen`,
|
|
|
80 |
// marginLeft: `${zoomLevel > 100 ? `100`}`
|
81 |
}}
|
82 |
>
|
83 |
+
<LayoutElement page={page} nbPanels={panelsPerPage} />
|
84 |
</div>
|
85 |
{currentNbPages > 1 &&
|
86 |
<p className="w-full text-center pt-4 font-sans text-2xs font-semibold text-stone-600">
|
87 |
+
{page + 1}/{maxNbPages}
|
88 |
{/*
|
89 |
+
alternative styles:
|
90 |
+
Page {page + 1}
|
91 |
+
Page {page + 1} / {maxNbPages}
|
92 |
+
{page + 1} / {maxNbPages}
|
93 |
*/}
|
94 |
</p>}
|
95 |
</div>
|
src/app/interface/panel/index.tsx
CHANGED
@@ -44,11 +44,12 @@ export function Panel({
|
|
44 |
// index of the panel in the whole app
|
45 |
const panelIndex = page * nbPanels + panel
|
46 |
|
47 |
-
|
48 |
// the panel Id must be unique across all pages
|
49 |
const panelId = `${panelIndex}`
|
50 |
|
51 |
-
// console.log(
|
|
|
52 |
|
53 |
const [mouseOver, setMouseOver] = useState(false)
|
54 |
const ref = useRef<HTMLImageElement>(null)
|
@@ -94,6 +95,18 @@ export function Panel({
|
|
94 |
|
95 |
let delay = enableRateLimiter ? (1000 + (500 * panelIndex)) : 1000
|
96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
97 |
// Let's be gentle with Replicate or else they will believe they are under attack
|
98 |
if (renderingModelVendor === "REPLICATE") {
|
99 |
delay += 8000
|
@@ -117,6 +130,7 @@ export function Panel({
|
|
117 |
nbFrames: number
|
118 |
revision: number
|
119 |
}) => {
|
|
|
120 |
if (!prompt?.length) { return }
|
121 |
|
122 |
// important: update the status, and clear the scene
|
@@ -133,7 +147,7 @@ export function Panel({
|
|
133 |
// atrocious and very, very, very, very, very, very, very ugly hack for the Inference API
|
134 |
// as apparently "use_cache: false" doesn't work, or doesn't do what we want it to do
|
135 |
let cacheInvalidationHack = ""
|
136 |
-
const nbMaxRevisions =
|
137 |
for (let i = 0; i < revision && revision < nbMaxRevisions; i++) {
|
138 |
const j = Math.random()
|
139 |
cacheInvalidationHack += j < 0.3 ? "_" : j < 0.6 ? "," : "-"
|
|
|
44 |
// index of the panel in the whole app
|
45 |
const panelIndex = page * nbPanels + panel
|
46 |
|
47 |
+
|
48 |
// the panel Id must be unique across all pages
|
49 |
const panelId = `${panelIndex}`
|
50 |
|
51 |
+
// console.log(`panel/index.tsx: <Panel panelId=${panelId}> rendered again!`)
|
52 |
+
|
53 |
|
54 |
const [mouseOver, setMouseOver] = useState(false)
|
55 |
const ref = useRef<HTMLImageElement>(null)
|
|
|
95 |
|
96 |
let delay = enableRateLimiter ? (1000 + (500 * panelIndex)) : 1000
|
97 |
|
98 |
+
/*
|
99 |
+
console.log("panel/index.tsx: DEBUG: " + JSON.stringify({
|
100 |
+
page,
|
101 |
+
nbPanels,
|
102 |
+
panel,
|
103 |
+
panelIndex,
|
104 |
+
panelId,
|
105 |
+
revision,
|
106 |
+
renderedScenes: Object.keys(renderedScenes),
|
107 |
+
}, null, 2))
|
108 |
+
*/
|
109 |
+
|
110 |
// Let's be gentle with Replicate or else they will believe they are under attack
|
111 |
if (renderingModelVendor === "REPLICATE") {
|
112 |
delay += 8000
|
|
|
130 |
nbFrames: number
|
131 |
revision: number
|
132 |
}) => {
|
133 |
+
console.log(`panel/index.tsx: startImageGeneration(${JSON.stringify({ prompt, width, height, nbFrames, revision }, null, 2)})`)
|
134 |
if (!prompt?.length) { return }
|
135 |
|
136 |
// important: update the status, and clear the scene
|
|
|
147 |
// atrocious and very, very, very, very, very, very, very ugly hack for the Inference API
|
148 |
// as apparently "use_cache: false" doesn't work, or doesn't do what we want it to do
|
149 |
let cacheInvalidationHack = ""
|
150 |
+
const nbMaxRevisions = 20
|
151 |
for (let i = 0; i < revision && revision < nbMaxRevisions; i++) {
|
152 |
const j = Math.random()
|
153 |
cacheInvalidationHack += j < 0.3 ? "_" : j < 0.6 ? "," : "-"
|
src/app/interface/settings-dialog/index.tsx
CHANGED
@@ -87,35 +87,35 @@ export function SettingsDialog() {
|
|
87 |
<DialogTrigger asChild>
|
88 |
<Button className="space-x-1 md:space-x-2">
|
89 |
<div>
|
90 |
-
<span className="hidden md:inline">
|
91 |
</div>
|
92 |
</Button>
|
93 |
</DialogTrigger>
|
94 |
-
<DialogContent className="w-full sm:max-w-[500px] md:max-w-[700px]
|
95 |
<DialogHeader>
|
96 |
<DialogDescription className="w-full text-center text-lg font-bold text-stone-800">
|
97 |
-
|
98 |
</DialogDescription>
|
99 |
</DialogHeader>
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
}
|
118 |
-
<div className="grid gap-4
|
119 |
<Field>
|
120 |
<Label>Image rendering provider:</Label>
|
121 |
<p className="pt-2 pb-3 text-base italic text-zinc-600">
|
@@ -301,6 +301,8 @@ export function SettingsDialog() {
|
|
301 |
</p>
|
302 |
</div>
|
303 |
|
|
|
|
|
304 |
<DialogFooter>
|
305 |
<Button type="submit" onClick={() => setOpen(false)}>Close</Button>
|
306 |
</DialogFooter>
|
|
|
87 |
<DialogTrigger asChild>
|
88 |
<Button className="space-x-1 md:space-x-2">
|
89 |
<div>
|
90 |
+
<span className="hidden md:inline">Settings</span>
|
91 |
</div>
|
92 |
</Button>
|
93 |
</DialogTrigger>
|
94 |
+
<DialogContent className="w-full sm:max-w-[500px] md:max-w-[700px]">
|
95 |
<DialogHeader>
|
96 |
<DialogDescription className="w-full text-center text-lg font-bold text-stone-800">
|
97 |
+
Settings
|
98 |
</DialogDescription>
|
99 |
</DialogHeader>
|
100 |
+
<div className="overflow-y-scroll h-[75vh] md:h-[70vh]">
|
101 |
+
{isConfigReady && <Field>
|
102 |
+
<Label>(new!) Control the number of pages: {userDefinedMaxNumberOfPages}</Label>
|
103 |
+
<Slider
|
104 |
+
min={1}
|
105 |
+
max={maxNbPages}
|
106 |
+
step={1}
|
107 |
+
onValueChange={(value: any) => {
|
108 |
+
let numericValue = Number(value[0])
|
109 |
+
numericValue = !isNaN(value[0]) && isFinite(value[0]) ? numericValue : 0
|
110 |
+
numericValue = Math.min(maxNbPages, Math.max(1, numericValue))
|
111 |
+
setUserDefinedMaxNumberOfPages(numericValue)
|
112 |
+
}}
|
113 |
+
defaultValue={[userDefinedMaxNumberOfPages]}
|
114 |
+
value={[userDefinedMaxNumberOfPages]}
|
115 |
+
/>
|
116 |
+
</Field>
|
117 |
}
|
118 |
+
<div className="grid gap-4 pt-8 pb-1 space-y-1 text-stone-800">
|
119 |
<Field>
|
120 |
<Label>Image rendering provider:</Label>
|
121 |
<p className="pt-2 pb-3 text-base italic text-zinc-600">
|
|
|
301 |
</p>
|
302 |
</div>
|
303 |
|
304 |
+
</div>
|
305 |
+
|
306 |
<DialogFooter>
|
307 |
<Button type="submit" onClick={() => setOpen(false)}>Close</Button>
|
308 |
</DialogFooter>
|
src/app/interface/sign-up-cta/sign-up-cta.tsx
CHANGED
@@ -7,7 +7,7 @@ function SignUpCTA() {
|
|
7 |
return (
|
8 |
<div className={cn(
|
9 |
`print:hidden`,
|
10 |
-
`fixed flex flex-col items-center bottom-
|
11 |
)}>
|
12 |
<div className="font-bold text-sm pb-2 text-stone-600 bg-stone-50 dark:text-stone-600 dark:bg-stone-50 p-1 rounded-sm">
|
13 |
anonymous users can generate 1 comic.<br/> <span
|
|
|
7 |
return (
|
8 |
<div className={cn(
|
9 |
`print:hidden`,
|
10 |
+
`fixed flex flex-col items-center bottom-24 top-28 right-2 md:top-17 md:right-6 z-10`,
|
11 |
)}>
|
12 |
<div className="font-bold text-sm pb-2 text-stone-600 bg-stone-50 dark:text-stone-600 dark:bg-stone-50 p-1 rounded-sm">
|
13 |
anonymous users can generate 1 comic.<br/> <span
|
src/app/interface/top-menu/index.tsx
CHANGED
@@ -51,6 +51,9 @@ export function TopMenu() {
|
|
51 |
const setShowCaptions = useStore(state => state.setShowCaptions)
|
52 |
const showCaptions = useStore(state => state.showCaptions)
|
53 |
|
|
|
|
|
|
|
54 |
const generate = useStore(state => state.generate)
|
55 |
|
56 |
const isGeneratingStory = useStore(state => state.isGeneratingStory)
|
@@ -102,7 +105,7 @@ export function TopMenu() {
|
|
102 |
setShowAuthWall(true)
|
103 |
return
|
104 |
}
|
105 |
-
|
106 |
const promptChanged = draftPrompt.trim() !== prompt.trim()
|
107 |
const presetChanged = draftPreset !== preset.id
|
108 |
const layoutChanged = draftLayout !== layout
|
|
|
51 |
const setShowCaptions = useStore(state => state.setShowCaptions)
|
52 |
const showCaptions = useStore(state => state.showCaptions)
|
53 |
|
54 |
+
const currentNbPages = useStore(state => state.currentNbPages)
|
55 |
+
const setCurrentNbPages = useStore(state => state.setCurrentNbPages)
|
56 |
+
|
57 |
const generate = useStore(state => state.generate)
|
58 |
|
59 |
const isGeneratingStory = useStore(state => state.isGeneratingStory)
|
|
|
105 |
setShowAuthWall(true)
|
106 |
return
|
107 |
}
|
108 |
+
|
109 |
const promptChanged = draftPrompt.trim() !== prompt.trim()
|
110 |
const presetChanged = draftPreset !== preset.id
|
111 |
const layoutChanged = draftLayout !== layout
|
src/app/main.tsx
CHANGED
@@ -1,11 +1,14 @@
|
|
1 |
"use client"
|
2 |
|
3 |
-
import { Suspense, useEffect, useState, useTransition } from "react"
|
|
|
4 |
|
5 |
import { cn } from "@/lib/utils"
|
6 |
import { fonts } from "@/lib/fonts"
|
7 |
import { GeneratedPanel } from "@/types"
|
8 |
import { joinWords } from "@/lib/joinWords"
|
|
|
|
|
9 |
|
10 |
import { TopMenu } from "./interface/top-menu"
|
11 |
import { useStore } from "./store"
|
@@ -13,12 +16,10 @@ import { Zoom } from "./interface/zoom"
|
|
13 |
import { BottomBar } from "./interface/bottom-bar"
|
14 |
import { Page } from "./interface/page"
|
15 |
import { getStoryContinuation } from "./queries/getStoryContinuation"
|
16 |
-
import { useDynamicConfig } from "@/lib/useDynamicConfig"
|
17 |
-
import { useLocalStorage } from "usehooks-ts"
|
18 |
import { localStorageKeys } from "./interface/settings-dialog/localStorageKeys"
|
19 |
import { defaultSettings } from "./interface/settings-dialog/defaultSettings"
|
20 |
-
import { Button } from "@/components/ui/button"
|
21 |
import { SignUpCTA } from "./interface/sign-up-cta"
|
|
|
22 |
|
23 |
export default function Main() {
|
24 |
const [_isPending, startTransition] = useTransition()
|
@@ -31,10 +32,9 @@ export default function Main() {
|
|
31 |
const preset = useStore(s => s.preset)
|
32 |
const prompt = useStore(s => s.prompt)
|
33 |
|
34 |
-
const currentNbPanelsPerPage = useStore(s => s.currentNbPanelsPerPage)
|
35 |
-
const maxNbPanelsPerPage = useStore(s => s.maxNbPanelsPerPage)
|
36 |
const currentNbPages = useStore(s => s.currentNbPages)
|
37 |
const maxNbPages = useStore(s => s.maxNbPages)
|
|
|
38 |
const currentNbPanels = useStore(s => s.currentNbPanels)
|
39 |
const maxNbPanels = useStore(s => s.maxNbPanels)
|
40 |
|
@@ -42,10 +42,14 @@ export default function Main() {
|
|
42 |
const setMaxNbPanelsPerPage = useStore(s => s.setMaxNbPanelsPerPage)
|
43 |
const setCurrentNbPages = useStore(s => s.setCurrentNbPages)
|
44 |
const setMaxNbPages = useStore(s => s.setMaxNbPages)
|
45 |
-
const setCurrentNbPanels = useStore(s => s.setCurrentNbPanels)
|
46 |
-
const setMaxNbPanels = useStore(s => s.setMaxNbPanels)
|
47 |
|
|
|
48 |
const setPanels = useStore(s => s.setPanels)
|
|
|
|
|
|
|
|
|
|
|
49 |
const setCaptions = useStore(s => s.setCaptions)
|
50 |
|
51 |
const zoomLevel = useStore(s => s.zoomLevel)
|
@@ -57,6 +61,35 @@ export default function Main() {
|
|
57 |
defaultSettings.userDefinedMaxNumberOfPages
|
58 |
)
|
59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
useEffect(() => {
|
61 |
if (maxNbPages !== userDefinedMaxNumberOfPages) {
|
62 |
setMaxNbPages(userDefinedMaxNumberOfPages)
|
@@ -64,6 +97,14 @@ export default function Main() {
|
|
64 |
}, [maxNbPages, userDefinedMaxNumberOfPages])
|
65 |
|
66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
useEffect(() => {
|
68 |
if (isConfigReady) {
|
69 |
|
@@ -76,15 +117,30 @@ export default function Main() {
|
|
76 |
|
77 |
// react to prompt changes
|
78 |
useEffect(() => {
|
|
|
79 |
if (!prompt) { return }
|
80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
startTransition(async () => {
|
82 |
setWaitABitMore(false)
|
83 |
setGeneratingStory(true)
|
84 |
|
85 |
-
// I don't think we are going to need a rate limiter on the LLM part anymore
|
86 |
-
const enableRateLimiter = false // `${process.env.NEXT_PUBLIC_ENABLE_RATE_LIMITER}` === "true"
|
87 |
-
|
88 |
const [stylePrompt, userStoryPrompt] = prompt.split("||").map(x => x.trim())
|
89 |
|
90 |
// we have to limit the size of the prompt, otherwise the rest of the style won't be followed
|
@@ -95,25 +151,30 @@ export default function Main() {
|
|
95 |
}
|
96 |
|
97 |
// new experimental prompt: let's drop the user prompt, and only use the style
|
98 |
-
const lightPanelPromptPrefix = joinWords(preset.imagePrompt(limitedStylePrompt))
|
99 |
|
100 |
// this prompt will be used if the LLM generation failed
|
101 |
-
const degradedPanelPromptPrefix = joinWords([
|
102 |
...preset.imagePrompt(limitedStylePrompt),
|
103 |
|
104 |
// we re-inject the story, then
|
105 |
userStoryPrompt
|
106 |
])
|
107 |
|
108 |
-
let existingPanels: GeneratedPanel[] = []
|
109 |
-
const newPanelsPrompts: string[] = []
|
110 |
-
const newCaptions: string[] = []
|
111 |
-
|
112 |
// we always generate panels 2 by 2
|
113 |
const nbPanelsToGenerate = 2
|
114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
115 |
for (
|
116 |
-
let currentPanel =
|
117 |
currentPanel < currentNbPanels;
|
118 |
currentPanel += nbPanelsToGenerate
|
119 |
) {
|
@@ -124,48 +185,55 @@ export default function Main() {
|
|
124 |
userStoryPrompt,
|
125 |
nbPanelsToGenerate,
|
126 |
maxNbPanels,
|
127 |
-
|
|
|
|
|
|
|
128 |
})
|
129 |
-
console.log("LLM generated some new panels:", candidatePanels)
|
130 |
|
131 |
-
existingPanels.push(...candidatePanels)
|
|
|
132 |
|
133 |
-
console.log(`
|
134 |
|
135 |
const startAt = currentPanel
|
136 |
const endAt = currentPanel + nbPanelsToGenerate
|
137 |
for (let p = startAt; p < endAt; p++) {
|
138 |
-
newCaptions.push(existingPanels[p]?.caption.trim() || "...")
|
139 |
const newPanel = joinWords([
|
140 |
|
141 |
// what we do here is that ideally we give full control to the LLM for prompting,
|
142 |
// unless there was a catastrophic failure, in that case we preserve the original prompt
|
143 |
-
existingPanels[p]?.instructions
|
144 |
? lightPanelPromptPrefix
|
145 |
: degradedPanelPromptPrefix,
|
146 |
|
147 |
-
existingPanels[p]?.instructions
|
148 |
])
|
149 |
-
newPanelsPrompts.push(newPanel)
|
150 |
|
151 |
-
console.log(`
|
152 |
}
|
153 |
|
154 |
// update the frontend
|
155 |
// console.log("updating the frontend..")
|
156 |
-
setCaptions(newCaptions)
|
157 |
-
setPanels(newPanelsPrompts)
|
158 |
|
159 |
setGeneratingStory(false)
|
160 |
} catch (err) {
|
161 |
-
console.log("
|
162 |
setGeneratingStory(false)
|
163 |
break
|
164 |
}
|
165 |
if (currentPanel > (currentNbPanels / 2)) {
|
166 |
-
console.log("
|
167 |
// setWaitABitMore(true)
|
168 |
}
|
|
|
|
|
|
|
169 |
}
|
170 |
|
171 |
/*
|
@@ -176,7 +244,13 @@ export default function Main() {
|
|
176 |
*/
|
177 |
|
178 |
})
|
179 |
-
}, [
|
|
|
|
|
|
|
|
|
|
|
|
|
180 |
|
181 |
return (
|
182 |
<Suspense>
|
@@ -205,11 +279,13 @@ export default function Main() {
|
|
205 |
{Array(currentNbPages).fill(0).map((_, i) => <Page key={i} page={i} />)}
|
206 |
</div>
|
207 |
{
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
|
|
|
|
213 |
}
|
214 |
</div>
|
215 |
</div>
|
|
|
1 |
"use client"
|
2 |
|
3 |
+
import { Suspense, useEffect, useRef, useState, useTransition } from "react"
|
4 |
+
import { useLocalStorage } from "usehooks-ts"
|
5 |
|
6 |
import { cn } from "@/lib/utils"
|
7 |
import { fonts } from "@/lib/fonts"
|
8 |
import { GeneratedPanel } from "@/types"
|
9 |
import { joinWords } from "@/lib/joinWords"
|
10 |
+
import { useDynamicConfig } from "@/lib/useDynamicConfig"
|
11 |
+
import { Button } from "@/components/ui/button"
|
12 |
|
13 |
import { TopMenu } from "./interface/top-menu"
|
14 |
import { useStore } from "./store"
|
|
|
16 |
import { BottomBar } from "./interface/bottom-bar"
|
17 |
import { Page } from "./interface/page"
|
18 |
import { getStoryContinuation } from "./queries/getStoryContinuation"
|
|
|
|
|
19 |
import { localStorageKeys } from "./interface/settings-dialog/localStorageKeys"
|
20 |
import { defaultSettings } from "./interface/settings-dialog/defaultSettings"
|
|
|
21 |
import { SignUpCTA } from "./interface/sign-up-cta"
|
22 |
+
import { sleep } from "@/lib/sleep"
|
23 |
|
24 |
export default function Main() {
|
25 |
const [_isPending, startTransition] = useTransition()
|
|
|
32 |
const preset = useStore(s => s.preset)
|
33 |
const prompt = useStore(s => s.prompt)
|
34 |
|
|
|
|
|
35 |
const currentNbPages = useStore(s => s.currentNbPages)
|
36 |
const maxNbPages = useStore(s => s.maxNbPages)
|
37 |
+
const previousNbPanels = useStore(s => s.previousNbPanels)
|
38 |
const currentNbPanels = useStore(s => s.currentNbPanels)
|
39 |
const maxNbPanels = useStore(s => s.maxNbPanels)
|
40 |
|
|
|
42 |
const setMaxNbPanelsPerPage = useStore(s => s.setMaxNbPanelsPerPage)
|
43 |
const setCurrentNbPages = useStore(s => s.setCurrentNbPages)
|
44 |
const setMaxNbPages = useStore(s => s.setMaxNbPages)
|
|
|
|
|
45 |
|
46 |
+
const panels = useStore(s => s.panels)
|
47 |
const setPanels = useStore(s => s.setPanels)
|
48 |
+
|
49 |
+
// do we need those?
|
50 |
+
const renderedScenes = useStore(s => s.renderedScenes)
|
51 |
+
const captions = useStore(s => s.captions)
|
52 |
+
|
53 |
const setCaptions = useStore(s => s.setCaptions)
|
54 |
|
55 |
const zoomLevel = useStore(s => s.zoomLevel)
|
|
|
61 |
defaultSettings.userDefinedMaxNumberOfPages
|
62 |
)
|
63 |
|
64 |
+
const numberOfPanels = Object.keys(panels).length
|
65 |
+
const panelGenerationStatus = useStore(state => state.panelGenerationStatus)
|
66 |
+
const allStatus = Object.values(panelGenerationStatus)
|
67 |
+
const numberOfPendingGenerations = allStatus.reduce((acc, s) => (acc + (s ? 1 : 0)), 0)
|
68 |
+
|
69 |
+
const hasAtLeastOnePage = numberOfPanels > 0
|
70 |
+
|
71 |
+
const hasNoPendingGeneration =
|
72 |
+
numberOfPendingGenerations === 0
|
73 |
+
|
74 |
+
const hasStillMorePagesToGenerate =
|
75 |
+
currentNbPages < maxNbPages
|
76 |
+
|
77 |
+
const showNextPageButton =
|
78 |
+
hasAtLeastOnePage &&
|
79 |
+
hasNoPendingGeneration &&
|
80 |
+
hasStillMorePagesToGenerate
|
81 |
+
|
82 |
+
/*
|
83 |
+
console.log("<Main>: " + JSON.stringify({
|
84 |
+
currentNbPages,
|
85 |
+
hasAtLeastOnePage,
|
86 |
+
numberOfPendingGenerations,
|
87 |
+
hasNoPendingGeneration,
|
88 |
+
hasStillMorePagesToGenerate,
|
89 |
+
showNextPageButton
|
90 |
+
}, null, 2))
|
91 |
+
*/
|
92 |
+
|
93 |
useEffect(() => {
|
94 |
if (maxNbPages !== userDefinedMaxNumberOfPages) {
|
95 |
setMaxNbPages(userDefinedMaxNumberOfPages)
|
|
|
97 |
}, [maxNbPages, userDefinedMaxNumberOfPages])
|
98 |
|
99 |
|
100 |
+
const ref = useRef({
|
101 |
+
existingPanels: [] as GeneratedPanel[],
|
102 |
+
newPanelsPrompts: [] as string[],
|
103 |
+
newCaptions: [] as string[],
|
104 |
+
prompt: "",
|
105 |
+
preset: "",
|
106 |
+
})
|
107 |
+
|
108 |
useEffect(() => {
|
109 |
if (isConfigReady) {
|
110 |
|
|
|
117 |
|
118 |
// react to prompt changes
|
119 |
useEffect(() => {
|
120 |
+
// console.log(`main.tsx: asked to re-generate!!`)
|
121 |
if (!prompt) { return }
|
122 |
|
123 |
+
// if the prompt or preset changed, we clear the cache
|
124 |
+
// this part is important, otherwise when trying to change the prompt
|
125 |
+
// we wouldn't still have remnants of the previous comic
|
126 |
+
// in the data sent to the LLM (also the page cursor would be wrong)
|
127 |
+
if (
|
128 |
+
prompt !== ref.current.prompt ||
|
129 |
+
preset?.label !== ref.current.preset) {
|
130 |
+
// console.log("overwriting ref.current!")
|
131 |
+
ref.current = {
|
132 |
+
existingPanels: [],
|
133 |
+
newPanelsPrompts: [],
|
134 |
+
newCaptions: [],
|
135 |
+
prompt,
|
136 |
+
preset: preset?.label || "",
|
137 |
+
}
|
138 |
+
}
|
139 |
+
|
140 |
startTransition(async () => {
|
141 |
setWaitABitMore(false)
|
142 |
setGeneratingStory(true)
|
143 |
|
|
|
|
|
|
|
144 |
const [stylePrompt, userStoryPrompt] = prompt.split("||").map(x => x.trim())
|
145 |
|
146 |
// we have to limit the size of the prompt, otherwise the rest of the style won't be followed
|
|
|
151 |
}
|
152 |
|
153 |
// new experimental prompt: let's drop the user prompt, and only use the style
|
154 |
+
const lightPanelPromptPrefix: string = joinWords(preset.imagePrompt(limitedStylePrompt))
|
155 |
|
156 |
// this prompt will be used if the LLM generation failed
|
157 |
+
const degradedPanelPromptPrefix: string = joinWords([
|
158 |
...preset.imagePrompt(limitedStylePrompt),
|
159 |
|
160 |
// we re-inject the story, then
|
161 |
userStoryPrompt
|
162 |
])
|
163 |
|
|
|
|
|
|
|
|
|
164 |
// we always generate panels 2 by 2
|
165 |
const nbPanelsToGenerate = 2
|
166 |
|
167 |
+
/*
|
168 |
+
console.log("going to call getStoryContinuation based on: " + JSON.stringify({
|
169 |
+
previousNbPanels,
|
170 |
+
currentNbPanels,
|
171 |
+
nbPanelsToGenerate,
|
172 |
+
"ref.current:": ref.current,
|
173 |
+
}, null, 2))
|
174 |
+
*/
|
175 |
+
|
176 |
for (
|
177 |
+
let currentPanel = previousNbPanels;
|
178 |
currentPanel < currentNbPanels;
|
179 |
currentPanel += nbPanelsToGenerate
|
180 |
) {
|
|
|
185 |
userStoryPrompt,
|
186 |
nbPanelsToGenerate,
|
187 |
maxNbPanels,
|
188 |
+
|
189 |
+
// existing panels are critical here: this is how we can
|
190 |
+
// continue over an existing story
|
191 |
+
existingPanels: ref.current.existingPanels,
|
192 |
})
|
193 |
+
// console.log("LLM generated some new panels:", candidatePanels)
|
194 |
|
195 |
+
ref.current.existingPanels.push(...candidatePanels)
|
196 |
+
// console.log("ref.current.existingPanels.push(...candidatePanels) successful, now we have ref.current.existingPanels = ", ref.current.existingPanels)
|
197 |
|
198 |
+
// console.log(`main.tsx: converting the ${nbPanelsToGenerate} new panels into image prompts..`)
|
199 |
|
200 |
const startAt = currentPanel
|
201 |
const endAt = currentPanel + nbPanelsToGenerate
|
202 |
for (let p = startAt; p < endAt; p++) {
|
203 |
+
ref.current.newCaptions.push(ref.current.existingPanels[p]?.caption.trim() || "...")
|
204 |
const newPanel = joinWords([
|
205 |
|
206 |
// what we do here is that ideally we give full control to the LLM for prompting,
|
207 |
// unless there was a catastrophic failure, in that case we preserve the original prompt
|
208 |
+
ref.current.existingPanels[p]?.instructions
|
209 |
? lightPanelPromptPrefix
|
210 |
: degradedPanelPromptPrefix,
|
211 |
|
212 |
+
ref.current.existingPanels[p]?.instructions || ""
|
213 |
])
|
214 |
+
ref.current.newPanelsPrompts.push(newPanel)
|
215 |
|
216 |
+
console.log(`main.tsx: image prompt for panel ${p} => "${newPanel}"`)
|
217 |
}
|
218 |
|
219 |
// update the frontend
|
220 |
// console.log("updating the frontend..")
|
221 |
+
setCaptions(ref.current.newCaptions)
|
222 |
+
setPanels(ref.current.newPanelsPrompts)
|
223 |
|
224 |
setGeneratingStory(false)
|
225 |
} catch (err) {
|
226 |
+
console.log("main.tsx: LLM generation failed:", err)
|
227 |
setGeneratingStory(false)
|
228 |
break
|
229 |
}
|
230 |
if (currentPanel > (currentNbPanels / 2)) {
|
231 |
+
console.log("main.tsx: we are halfway there, hold tight!")
|
232 |
// setWaitABitMore(true)
|
233 |
}
|
234 |
+
|
235 |
+
// we could sleep here if we want to
|
236 |
+
// await sleep(1000)
|
237 |
}
|
238 |
|
239 |
/*
|
|
|
244 |
*/
|
245 |
|
246 |
})
|
247 |
+
}, [
|
248 |
+
prompt,
|
249 |
+
preset?.label,
|
250 |
+
previousNbPanels,
|
251 |
+
currentNbPanels,
|
252 |
+
maxNbPanels
|
253 |
+
]) // important: we need to react to preset changes too
|
254 |
|
255 |
return (
|
256 |
<Suspense>
|
|
|
279 |
{Array(currentNbPages).fill(0).map((_, i) => <Page key={i} page={i} />)}
|
280 |
</div>
|
281 |
{
|
282 |
+
showNextPageButton &&
|
283 |
+
<div className="flex flex-col space-y-2 pt-2 pb-6 text-gray-600 dark:text-gray-600">
|
284 |
+
<div>Happy with your story?</div>
|
285 |
+
<div>You can <Button onClick={() => {
|
286 |
+
setCurrentNbPages(currentNbPages + 1)
|
287 |
+
}}>Add page {currentNbPages + 1} 👀</Button></div>
|
288 |
+
</div>
|
289 |
}
|
290 |
</div>
|
291 |
</div>
|
src/app/queries/getStoryContinuation.ts
CHANGED
@@ -2,20 +2,21 @@ import { Preset } from "../engine/presets"
|
|
2 |
import { GeneratedPanel } from "@/types"
|
3 |
import { predictNextPanels } from "./predictNextPanels"
|
4 |
import { joinWords } from "@/lib/joinWords"
|
|
|
5 |
|
6 |
export const getStoryContinuation = async ({
|
7 |
preset,
|
8 |
stylePrompt = "",
|
9 |
userStoryPrompt = "",
|
10 |
-
nbPanelsToGenerate
|
11 |
-
maxNbPanels
|
12 |
existingPanels = [],
|
13 |
}: {
|
14 |
preset: Preset;
|
15 |
stylePrompt?: string;
|
16 |
userStoryPrompt?: string;
|
17 |
-
nbPanelsToGenerate
|
18 |
-
maxNbPanels
|
19 |
existingPanels?: GeneratedPanel[];
|
20 |
}): Promise<GeneratedPanel[]> => {
|
21 |
|
@@ -63,6 +64,7 @@ export const getStoryContinuation = async ({
|
|
63 |
caption: "(Sorry, LLM generation failed: using degraded mode)"
|
64 |
})
|
65 |
}
|
|
|
66 |
// console.error(err)
|
67 |
} finally {
|
68 |
return panels
|
|
|
2 |
import { GeneratedPanel } from "@/types"
|
3 |
import { predictNextPanels } from "./predictNextPanels"
|
4 |
import { joinWords } from "@/lib/joinWords"
|
5 |
+
import { sleep } from "@/lib/sleep"
|
6 |
|
7 |
export const getStoryContinuation = async ({
|
8 |
preset,
|
9 |
stylePrompt = "",
|
10 |
userStoryPrompt = "",
|
11 |
+
nbPanelsToGenerate,
|
12 |
+
maxNbPanels,
|
13 |
existingPanels = [],
|
14 |
}: {
|
15 |
preset: Preset;
|
16 |
stylePrompt?: string;
|
17 |
userStoryPrompt?: string;
|
18 |
+
nbPanelsToGenerate: number;
|
19 |
+
maxNbPanels: number;
|
20 |
existingPanels?: GeneratedPanel[];
|
21 |
}): Promise<GeneratedPanel[]> => {
|
22 |
|
|
|
64 |
caption: "(Sorry, LLM generation failed: using degraded mode)"
|
65 |
})
|
66 |
}
|
67 |
+
await sleep(2000)
|
68 |
// console.error(err)
|
69 |
} finally {
|
70 |
return panels
|
src/app/queries/predictNextPanels.ts
CHANGED
@@ -11,14 +11,14 @@ import { sleep } from "@/lib/sleep"
|
|
11 |
export const predictNextPanels = async ({
|
12 |
preset,
|
13 |
prompt = "",
|
14 |
-
nbPanelsToGenerate
|
15 |
-
maxNbPanels
|
16 |
existingPanels = [],
|
17 |
}: {
|
18 |
preset: Preset;
|
19 |
prompt: string;
|
20 |
-
nbPanelsToGenerate
|
21 |
-
maxNbPanels
|
22 |
existingPanels: GeneratedPanel[];
|
23 |
}): Promise<GeneratedPanel[]> => {
|
24 |
// console.log("predictNextPanels: ", { prompt, nbPanelsToGenerate })
|
|
|
11 |
export const predictNextPanels = async ({
|
12 |
preset,
|
13 |
prompt = "",
|
14 |
+
nbPanelsToGenerate,
|
15 |
+
maxNbPanels,
|
16 |
existingPanels = [],
|
17 |
}: {
|
18 |
preset: Preset;
|
19 |
prompt: string;
|
20 |
+
nbPanelsToGenerate: number;
|
21 |
+
maxNbPanels: number;
|
22 |
existingPanels: GeneratedPanel[];
|
23 |
}): Promise<GeneratedPanel[]> => {
|
24 |
// console.log("predictNextPanels: ", { prompt, nbPanelsToGenerate })
|
src/app/store/index.ts
CHANGED
@@ -16,6 +16,7 @@ export const useStore = create<{
|
|
16 |
maxNbPanelsPerPage: number
|
17 |
currentNbPages: number
|
18 |
maxNbPages: number
|
|
|
19 |
currentNbPanels: number
|
20 |
maxNbPanels: number
|
21 |
panels: string[]
|
@@ -36,6 +37,7 @@ export const useStore = create<{
|
|
36 |
setMaxNbPanelsPerPage: (maxNbPanelsPerPage: number) => void
|
37 |
setCurrentNbPages: (currentNbPages: number) => void
|
38 |
setMaxNbPages: (maxNbPages: number) => void
|
|
|
39 |
setCurrentNbPanels: (currentNbPanels: number) => void
|
40 |
setMaxNbPanels: (maxNbPanels: number) => void
|
41 |
|
@@ -53,12 +55,19 @@ export const useStore = create<{
|
|
53 |
setCaptions: (captions: string[]) => void
|
54 |
setPanelCaption: (newCaption: string, index: number) => void
|
55 |
setZoomLevel: (zoomLevel: number) => void
|
56 |
-
|
57 |
setGeneratingStory: (isGeneratingStory: boolean) => void
|
58 |
setGeneratingImages: (panelId: string, value: boolean) => void
|
59 |
setGeneratingText: (isGeneratingText: boolean) => void
|
60 |
-
|
61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
62 |
generate: (prompt: string, presetName: PresetName, layoutName: LayoutName) => void
|
63 |
}>((set, get) => ({
|
64 |
prompt: "",
|
@@ -69,6 +78,7 @@ export const useStore = create<{
|
|
69 |
maxNbPanelsPerPage: 4,
|
70 |
currentNbPages: 1,
|
71 |
maxNbPages: 1,
|
|
|
72 |
currentNbPanels: 4,
|
73 |
maxNbPanels: 4,
|
74 |
|
@@ -77,16 +87,22 @@ export const useStore = create<{
|
|
77 |
upscaleQueue: {} as Record<string, RenderedScene>,
|
78 |
renderedScenes: {} as Record<string, RenderedScene>,
|
79 |
showCaptions: false,
|
|
|
|
|
80 |
layout: defaultLayout,
|
81 |
-
|
|
|
|
|
82 |
zoomLevel: 60,
|
|
|
|
|
83 |
page: undefined as unknown as HTMLDivElement,
|
|
|
84 |
isGeneratingStory: false,
|
85 |
panelGenerationStatus: {},
|
86 |
isGeneratingText: false,
|
87 |
atLeastOnePanelIsBusy: false,
|
88 |
|
89 |
-
|
90 |
setCurrentNbPanelsPerPage: (currentNbPanelsPerPage: number) => {
|
91 |
const { currentNbPages } = get()
|
92 |
set({
|
@@ -102,10 +118,41 @@ export const useStore = create<{
|
|
102 |
})
|
103 |
},
|
104 |
setCurrentNbPages: (currentNbPages: number) => {
|
105 |
-
const
|
106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
107 |
currentNbPages,
|
108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
})
|
110 |
},
|
111 |
setMaxNbPages: (maxNbPages: number) => {
|
@@ -115,8 +162,40 @@ export const useStore = create<{
|
|
115 |
maxNbPanels: maxNbPanelsPerPage * maxNbPages,
|
116 |
})
|
117 |
},
|
|
|
|
|
|
|
|
|
|
|
118 |
setCurrentNbPanels: (currentNbPanels: number) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
119 |
set({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
120 |
currentNbPanels,
|
121 |
})
|
122 |
},
|
@@ -200,35 +279,37 @@ export const useStore = create<{
|
|
200 |
})
|
201 |
},
|
202 |
setLayout: (layoutName: LayoutName) => {
|
203 |
-
|
204 |
-
const { currentNbPages } = get()
|
205 |
-
|
206 |
-
const layout = layoutName === "random"
|
207 |
-
? getRandomLayoutName()
|
208 |
-
: layoutName
|
209 |
|
210 |
const layouts: LayoutName[] = []
|
211 |
-
for (let i = 0; i <
|
212 |
layouts.push(
|
213 |
layoutName === "random"
|
214 |
? getRandomLayoutName()
|
215 |
-
: layoutName
|
216 |
-
)
|
217 |
-
|
218 |
-
// TODO: update the number of total panels here!
|
219 |
}
|
220 |
|
221 |
set({
|
222 |
-
layout,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
223 |
layouts,
|
|
|
224 |
})
|
225 |
},
|
226 |
setLayouts: (layouts: LayoutName[]) => set({ layouts }),
|
227 |
setZoomLevel: (zoomLevel: number) => set({ zoomLevel }),
|
228 |
-
setPage: (page: HTMLDivElement) => {
|
229 |
-
if (!page) { return }
|
230 |
-
set({ page })
|
231 |
-
},
|
232 |
setGeneratingStory: (isGeneratingStory: boolean) => set({ isGeneratingStory }),
|
233 |
setGeneratingImages: (panelId: string, value: boolean) => {
|
234 |
const panelGenerationStatus: Record<string, boolean> = {
|
@@ -244,6 +325,16 @@ export const useStore = create<{
|
|
244 |
})
|
245 |
},
|
246 |
setGeneratingText: (isGeneratingText: boolean) => set({ isGeneratingText }),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
247 |
pageToImage: async () => {
|
248 |
const { page } = get()
|
249 |
if (!page) { return "" }
|
@@ -271,33 +362,37 @@ export const useStore = create<{
|
|
271 |
window.open(data)
|
272 |
}
|
273 |
},
|
|
|
274 |
generate: (prompt: string, presetName: PresetName, layoutName: LayoutName) => {
|
275 |
-
|
276 |
-
const { currentNbPages } = get()
|
277 |
-
|
278 |
-
const layout = layoutName === "random"
|
279 |
-
? getRandomLayoutName()
|
280 |
-
: layoutName
|
281 |
|
282 |
const layouts: LayoutName[] = []
|
283 |
-
for (let i = 0; i <
|
284 |
layouts.push(
|
285 |
layoutName === "random"
|
286 |
? getRandomLayoutName()
|
287 |
-
: layoutName
|
288 |
-
)
|
289 |
-
|
290 |
-
// TODO: update the number of total panels here!
|
291 |
}
|
292 |
|
293 |
set({
|
294 |
-
|
|
|
|
|
|
|
295 |
panels: [],
|
296 |
captions: [],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
297 |
preset: presetName === "random"
|
298 |
? getRandomPreset()
|
299 |
: getPreset(presetName),
|
300 |
-
layout,
|
301 |
layouts,
|
302 |
})
|
303 |
}
|
|
|
16 |
maxNbPanelsPerPage: number
|
17 |
currentNbPages: number
|
18 |
maxNbPages: number
|
19 |
+
previousNbPanels: number
|
20 |
currentNbPanels: number
|
21 |
maxNbPanels: number
|
22 |
panels: string[]
|
|
|
37 |
setMaxNbPanelsPerPage: (maxNbPanelsPerPage: number) => void
|
38 |
setCurrentNbPages: (currentNbPages: number) => void
|
39 |
setMaxNbPages: (maxNbPages: number) => void
|
40 |
+
setPreviousNbPanels: (previousNbPanels: number) => void
|
41 |
setCurrentNbPanels: (currentNbPanels: number) => void
|
42 |
setMaxNbPanels: (maxNbPanels: number) => void
|
43 |
|
|
|
55 |
setCaptions: (captions: string[]) => void
|
56 |
setPanelCaption: (newCaption: string, index: number) => void
|
57 |
setZoomLevel: (zoomLevel: number) => void
|
58 |
+
|
59 |
setGeneratingStory: (isGeneratingStory: boolean) => void
|
60 |
setGeneratingImages: (panelId: string, value: boolean) => void
|
61 |
setGeneratingText: (isGeneratingText: boolean) => void
|
62 |
+
|
63 |
+
// I think we should deprecate those three functions
|
64 |
+
// this was used to keep track of the page HTML element,
|
65 |
+
// for use with a HTML-to-bitmap library
|
66 |
+
// but the CSS layout wasn't followed properly and it depended on the zoom level
|
67 |
+
// pageToImage: () => Promise<string>
|
68 |
+
// download: () => Promise<void>
|
69 |
+
// setPage: (page: HTMLDivElement) => void
|
70 |
+
|
71 |
generate: (prompt: string, presetName: PresetName, layoutName: LayoutName) => void
|
72 |
}>((set, get) => ({
|
73 |
prompt: "",
|
|
|
78 |
maxNbPanelsPerPage: 4,
|
79 |
currentNbPages: 1,
|
80 |
maxNbPages: 1,
|
81 |
+
previousNbPanels: 0,
|
82 |
currentNbPanels: 4,
|
83 |
maxNbPanels: 4,
|
84 |
|
|
|
87 |
upscaleQueue: {} as Record<string, RenderedScene>,
|
88 |
renderedScenes: {} as Record<string, RenderedScene>,
|
89 |
showCaptions: false,
|
90 |
+
|
91 |
+
// deprecated?
|
92 |
layout: defaultLayout,
|
93 |
+
|
94 |
+
layouts: [defaultLayout, defaultLayout, defaultLayout, defaultLayout],
|
95 |
+
|
96 |
zoomLevel: 60,
|
97 |
+
|
98 |
+
// deprecated?
|
99 |
page: undefined as unknown as HTMLDivElement,
|
100 |
+
|
101 |
isGeneratingStory: false,
|
102 |
panelGenerationStatus: {},
|
103 |
isGeneratingText: false,
|
104 |
atLeastOnePanelIsBusy: false,
|
105 |
|
|
|
106 |
setCurrentNbPanelsPerPage: (currentNbPanelsPerPage: number) => {
|
107 |
const { currentNbPages } = get()
|
108 |
set({
|
|
|
118 |
})
|
119 |
},
|
120 |
setCurrentNbPages: (currentNbPages: number) => {
|
121 |
+
const state = get()
|
122 |
+
|
123 |
+
const newCurrentNumberOfPages = Math.min(state.maxNbPages, currentNbPages)
|
124 |
+
|
125 |
+
const newCurrentNbPanels = state.currentNbPanelsPerPage * newCurrentNumberOfPages
|
126 |
+
|
127 |
+
/*
|
128 |
+
console.log(`setCurrentNbPages(${currentNbPages}): ${JSON.stringify({
|
129 |
+
"state.maxNbPages": state.maxNbPages,
|
130 |
currentNbPages,
|
131 |
+
newCurrentNumberOfPages,
|
132 |
+
"state.currentNbPanelsPerPage": state.currentNbPanelsPerPage,
|
133 |
+
newCurrentNbPanels,
|
134 |
+
"state.currentNbPanels": state.currentNbPanels,
|
135 |
+
"state.previousNbPanels": state.previousNbPanels,
|
136 |
+
previousNbPanels:
|
137 |
+
newCurrentNbPanels > state.currentNbPanels ? state.currentNbPanels :
|
138 |
+
newCurrentNbPanels < state.currentNbPanels ? 0 :
|
139 |
+
state.previousNbPanels,
|
140 |
+
|
141 |
+
}, null, 2)}`)
|
142 |
+
*/
|
143 |
+
|
144 |
+
set({
|
145 |
+
// we keep the previous number of panels for convenience
|
146 |
+
// so if we are adding a new panel,
|
147 |
+
// state.currentNbPanels gets copied to state.previousNbPanels
|
148 |
+
previousNbPanels:
|
149 |
+
newCurrentNbPanels > state.currentNbPanels ? state.currentNbPanels :
|
150 |
+
newCurrentNbPanels < state.currentNbPanels ? 0 :
|
151 |
+
state.previousNbPanels,
|
152 |
+
|
153 |
+
currentNbPanels: newCurrentNbPanels,
|
154 |
+
currentNbPages: newCurrentNumberOfPages,
|
155 |
+
|
156 |
})
|
157 |
},
|
158 |
setMaxNbPages: (maxNbPages: number) => {
|
|
|
162 |
maxNbPanels: maxNbPanelsPerPage * maxNbPages,
|
163 |
})
|
164 |
},
|
165 |
+
setPreviousNbPanels: (previousNbPanels: number) => {
|
166 |
+
set({
|
167 |
+
previousNbPanels
|
168 |
+
})
|
169 |
+
},
|
170 |
setCurrentNbPanels: (currentNbPanels: number) => {
|
171 |
+
const state = get()
|
172 |
+
|
173 |
+
|
174 |
+
/*
|
175 |
+
console.log(`setCurrentNbPanels(${currentNbPanels}): ${JSON.stringify({
|
176 |
+
"state.maxNbPages": state.maxNbPages,
|
177 |
+
"state.currentNbPages": state.currentNbPages,
|
178 |
+
currentNbPanels,
|
179 |
+
"state.currentNbPanelsPerPage": state.currentNbPanelsPerPage,
|
180 |
+
"state.currentNbPanels": state.currentNbPanels,
|
181 |
+
"state.previousNbPanels": state.previousNbPanels,
|
182 |
+
previousNbPanels:
|
183 |
+
currentNbPanels > state.currentNbPanels ? state.currentNbPanels :
|
184 |
+
currentNbPanels < state.currentNbPanels ? 0 :
|
185 |
+
state.previousNbPanels,
|
186 |
+
|
187 |
+
}, null, 2)}`)
|
188 |
+
*/
|
189 |
+
|
190 |
set({
|
191 |
+
// we keep the previous number of panels for convenience
|
192 |
+
// so if we are adding a new panel,
|
193 |
+
// state.currentNbPanels gets copied to state.previousNbPanels
|
194 |
+
previousNbPanels:
|
195 |
+
currentNbPanels > state.currentNbPanels ? state.currentNbPanels :
|
196 |
+
currentNbPanels < state.currentNbPanels ? 0 :
|
197 |
+
state.previousNbPanels,
|
198 |
+
|
199 |
currentNbPanels,
|
200 |
})
|
201 |
},
|
|
|
279 |
})
|
280 |
},
|
281 |
setLayout: (layoutName: LayoutName) => {
|
282 |
+
const { maxNbPages, currentNbPanelsPerPage } = get()
|
|
|
|
|
|
|
|
|
|
|
283 |
|
284 |
const layouts: LayoutName[] = []
|
285 |
+
for (let i = 0; i < maxNbPages; i++) {
|
286 |
layouts.push(
|
287 |
layoutName === "random"
|
288 |
? getRandomLayoutName()
|
289 |
+
: layoutName)
|
|
|
|
|
|
|
290 |
}
|
291 |
|
292 |
set({
|
293 |
+
// changing the layout isn't a free pass to generate tons of panels at once,
|
294 |
+
// so we reset pretty much everything
|
295 |
+
previousNbPanels: 0,
|
296 |
+
currentNbPages: 1,
|
297 |
+
currentNbPanels: currentNbPanelsPerPage,
|
298 |
+
panels: [],
|
299 |
+
captions: [],
|
300 |
+
upscaleQueue: {},
|
301 |
+
renderedScenes: {},
|
302 |
+
isGeneratingStory: false,
|
303 |
+
panelGenerationStatus: {},
|
304 |
+
isGeneratingText: false,
|
305 |
+
atLeastOnePanelIsBusy: false,
|
306 |
+
|
307 |
layouts,
|
308 |
+
layout: layouts[0],
|
309 |
})
|
310 |
},
|
311 |
setLayouts: (layouts: LayoutName[]) => set({ layouts }),
|
312 |
setZoomLevel: (zoomLevel: number) => set({ zoomLevel }),
|
|
|
|
|
|
|
|
|
313 |
setGeneratingStory: (isGeneratingStory: boolean) => set({ isGeneratingStory }),
|
314 |
setGeneratingImages: (panelId: string, value: boolean) => {
|
315 |
const panelGenerationStatus: Record<string, boolean> = {
|
|
|
325 |
})
|
326 |
},
|
327 |
setGeneratingText: (isGeneratingText: boolean) => set({ isGeneratingText }),
|
328 |
+
|
329 |
+
// I think we should deprecate those three functions
|
330 |
+
// this was used to keep track of the page HTML element,
|
331 |
+
// for use with a HTML-to-bitmap library
|
332 |
+
// but the CSS layout wasn't followed properly and it depended on the zoom level
|
333 |
+
/*
|
334 |
+
setPage: (page: HTMLDivElement) => {
|
335 |
+
if (!page) { return }
|
336 |
+
set({ page })
|
337 |
+
},
|
338 |
pageToImage: async () => {
|
339 |
const { page } = get()
|
340 |
if (!page) { return "" }
|
|
|
362 |
window.open(data)
|
363 |
}
|
364 |
},
|
365 |
+
*/
|
366 |
generate: (prompt: string, presetName: PresetName, layoutName: LayoutName) => {
|
367 |
+
const { maxNbPages, currentNbPanelsPerPage } = get()
|
|
|
|
|
|
|
|
|
|
|
368 |
|
369 |
const layouts: LayoutName[] = []
|
370 |
+
for (let i = 0; i < maxNbPages; i++) {
|
371 |
layouts.push(
|
372 |
layoutName === "random"
|
373 |
? getRandomLayoutName()
|
374 |
+
: layoutName)
|
|
|
|
|
|
|
375 |
}
|
376 |
|
377 |
set({
|
378 |
+
// we reset pretty much everything
|
379 |
+
previousNbPanels: 0,
|
380 |
+
currentNbPages: 1,
|
381 |
+
currentNbPanels: currentNbPanelsPerPage,
|
382 |
panels: [],
|
383 |
captions: [],
|
384 |
+
upscaleQueue: {},
|
385 |
+
renderedScenes: {},
|
386 |
+
isGeneratingStory: false,
|
387 |
+
panelGenerationStatus: {},
|
388 |
+
isGeneratingText: false,
|
389 |
+
atLeastOnePanelIsBusy: false,
|
390 |
+
|
391 |
+
prompt,
|
392 |
preset: presetName === "random"
|
393 |
? getRandomPreset()
|
394 |
: getPreset(presetName),
|
395 |
+
layout: layouts[0],
|
396 |
layouts,
|
397 |
})
|
398 |
}
|