jbilcke-hf HF staff commited on
Commit
cb3fdda
1 Parent(s): 35380c3

adding some counter measures to reduce the pressure on the server

Browse files
public/bubble.jpg ADDED
public/mask.png ADDED
scripts/test.js DELETED
@@ -1,23 +0,0 @@
1
- const { promises: fs } = require("node:fs")
2
-
3
- const main = async () => {
4
- console.log('generating shot..')
5
- const response = await fetch("http://localhost:3000/api/shot", {
6
- method: "POST",
7
- headers: {
8
- "Accept": "application/json",
9
- "Content-Type": "application/json"
10
- },
11
- body: JSON.stringify({
12
- token: process.env.VC_SECRET_ACCESS_TOKEN,
13
- shotPrompt: "video of a dancing cat"
14
- })
15
- });
16
-
17
- console.log('response:', response)
18
- const buffer = await response.buffer()
19
-
20
- fs.writeFile(`./test-juju.mp4`, buffer)
21
- }
22
-
23
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/engine/render.ts CHANGED
@@ -51,14 +51,16 @@ export async function newRender({
51
  // negativePrompt, unused for now
52
  nbFrames: 1,
53
  nbSteps: 25, // 20 = fast, 30 = better, 50 = best
54
- actionnables: [],
55
- segmentation: "disabled", // one day we will remove this param, to make it automatic
56
  width,
57
  height,
58
 
59
  // no need to upscale right now as we generate tiny panels
60
  // maybe later we can provide an "export" button to PDF
61
- upscalingFactor: 2,
 
 
62
 
63
  // analyzing doesn't work yet, it seems..
64
  analyze: false, // analyze: true,
 
51
  // negativePrompt, unused for now
52
  nbFrames: 1,
53
  nbSteps: 25, // 20 = fast, 30 = better, 50 = best
54
+ actionnables: [], // ["text block"],
55
+ segmentation: "disabled", // "firstframe", // one day we will remove this param, to make it automatic
56
  width,
57
  height,
58
 
59
  // no need to upscale right now as we generate tiny panels
60
  // maybe later we can provide an "export" button to PDF
61
+ // unfortunately there are too many requests for upscaling,
62
+ // the server is always down
63
+ upscalingFactor: 1, // 2,
64
 
65
  // analyzing doesn't work yet, it seems..
66
  analyze: false, // analyze: true,
src/app/interface/bottom-bar/index.tsx CHANGED
@@ -28,7 +28,9 @@ export function BottomBar() {
28
  onClick={handlePrint}
29
  disabled={!prompt?.length}
30
  >{
31
- remainingImages ? `Print (${allStatus.length - remainingImages}/4 in HD ⌛)` : `Print (in HD)`
 
 
32
  }</Button>
33
  </div>
34
  </div>
 
28
  onClick={handlePrint}
29
  disabled={!prompt?.length}
30
  >{
31
+ remainingImages ? `${allStatus.length - remainingImages}/4 panels ⌛` : `Print as PDF`
32
+ // remainingImages ? `Print (${allStatus.length - remainingImages}/4 in HD ⌛)` : `Print (in HD)`
33
+
34
  }</Button>
35
  </div>
36
  </div>
src/app/interface/page/index.tsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { allLayouts } from "@/app/layouts"
2
+ import { useStore } from "@/app/store"
3
+ import { cn } from "@/lib/utils"
4
+ import { useEffect, useState } from "react"
5
+
6
+
7
+ export function Page({ page }: { page: number }) {
8
+ const zoomLevel = useStore(state => state.zoomLevel)
9
+ const layouts = useStore(state => state.layouts)
10
+ const prompt = useStore(state => state.prompt)
11
+
12
+ const LayoutElement = (allLayouts as any)[layouts[page]]
13
+
14
+ /*
15
+ const [canLoad, setCanLoad] = useState(false)
16
+ useEffect(() => {
17
+ if (prompt?.length) {
18
+ setCanLoad(false)
19
+ setTimeout(() => {
20
+ setCanLoad(true)
21
+ }, page * 4000)
22
+ }
23
+ }, [prompt])
24
+ */
25
+
26
+ return (
27
+ <div
28
+ className={cn(
29
+ `w-full`,
30
+ // we are trying to reach a "book" look
31
+ // we are using aspect-[297/210] because it matches A4 (297mm x 210mm)
32
+ // `aspect-[210/297]`,
33
+ `aspect-[250/297]`,
34
+
35
+ `transition-all duration-100 ease-in-out`,
36
+ `border border-stone-200`,
37
+ `shadow-2xl`,
38
+ `print:shadow-none`,
39
+ `print:border-0`,
40
+ `print:width-screen`
41
+ )}
42
+ style={{
43
+ padding: `${Math.round((zoomLevel / 100) * 16)}px`
44
+ // marginLeft: `${zoomLevel > 100 ? `100`}`
45
+ }}
46
+ >
47
+ <LayoutElement />
48
+ </div>
49
+ )
50
+ }
src/app/interface/panel/index.tsx CHANGED
@@ -12,6 +12,7 @@ import { cn } from "@/lib/utils"
12
  import { getInitialRenderedScene } from "@/lib/getInitialRenderedScene"
13
  import { Progress } from "@/app/interface/progress"
14
  import { see } from "@/app/engine/caption"
 
15
  // import { Bubble } from "./bubble"
16
 
17
  export function Panel({
@@ -49,23 +50,25 @@ export function Panel({
49
  // since this run in its own loop, we need to use references everywhere
50
  // but perhaps this could be refactored
51
  useEffect(() => {
52
- startTransition(async () => {
53
- // console.log("Panel prompt: "+ prompt)
54
- if (!prompt?.length) { return }
55
-
56
- console.log("Loading panel..")
57
 
58
- // console.log("calling:\nconst newRendered = await newRender({ prompt, preset, width, height })")
59
- console.log({
60
- panel, prompt, width, height
61
- })
62
 
63
- console.log("")
64
- // important: update the status, and clear the scene
65
- setGeneratingImages(panel, true)
66
- setRendered(getInitialRenderedScene())
67
 
68
- const newRendered = await newRender({ prompt, width, height })
 
 
 
 
 
 
 
 
69
 
70
  if (newRendered) {
71
  // console.log("newRendered:", newRendered)
@@ -86,6 +89,7 @@ export function Panel({
86
  return
87
  }
88
  })
 
89
  }, [prompt, width, height])
90
 
91
 
@@ -94,7 +98,7 @@ export function Panel({
94
  clearTimeout(timeoutRef.current)
95
 
96
  if (!renderedRef.current?.renderId || renderedRef.current?.status !== "pending") {
97
- timeoutRef.current = setTimeout(checkStatus, 1000)
98
  return
99
  }
100
  try {
@@ -112,20 +116,20 @@ export function Panel({
112
 
113
  if (newRendered.status === "pending") {
114
  // console.log("job not finished")
115
- timeoutRef.current = setTimeout(checkStatus, 1000)
116
  } else {
117
  console.log("panel finished!")
118
  setGeneratingImages(panel, false)
119
  }
120
  } catch (err) {
121
  console.error(err)
122
- timeoutRef.current = setTimeout(checkStatus, 1000)
123
  }
124
  })
125
  }
126
 
127
  useEffect(() => {
128
- console.log("starting timeout")
129
  clearTimeout(timeoutRef.current)
130
 
131
  // normally it should reply in < 1sec, but we could also use an interval
@@ -176,6 +180,22 @@ export function Panel({
176
  `print:border-[1.5px] print:shadow-none`,
177
  )
178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  if (prompt && !rendered.assetUrl) {
180
  return (
181
  <div className={cn(
@@ -194,11 +214,13 @@ export function Panel({
194
  { "grayscale": preset.color === "grayscale" },
195
  className
196
  )}>
197
- {rendered.assetUrl && <img
198
- src={rendered.assetUrl}
199
- className="w-full h-full object-cover"
200
- alt={rendered.alt}
201
- />}
 
 
202
 
203
  {/*<Bubble className="absolute top-4 left-4">
204
  Hello, world!
 
12
  import { getInitialRenderedScene } from "@/lib/getInitialRenderedScene"
13
  import { Progress } from "@/app/interface/progress"
14
  import { see } from "@/app/engine/caption"
15
+ import { writeIntoBubble } from "@/lib/writeIntoBubble"
16
  // import { Bubble } from "./bubble"
17
 
18
  export function Panel({
 
50
  // since this run in its own loop, we need to use references everywhere
51
  // but perhaps this could be refactored
52
  useEffect(() => {
53
+ // console.log("Panel prompt: "+ prompt)
54
+ if (!prompt?.length) { return }
 
 
 
55
 
56
+ // important: update the status, and clear the scene
57
+ setGeneratingImages(panel, true)
58
+ setRendered(getInitialRenderedScene())
 
59
 
60
+ setTimeout(() => {
61
+ startTransition(async () => {
 
 
62
 
63
+ console.log(`Loading panel ${panel}..`)
64
+
65
+ let newRendered = await newRender({ prompt, width, height })
66
+ try {
67
+ newRendered = await newRender({ prompt, width, height })
68
+ } catch (err) {
69
+ console.log("Failed to load the panel! Don't worry, we are retrying..")
70
+ newRendered = await newRender({ prompt, width, height })
71
+ }
72
 
73
  if (newRendered) {
74
  // console.log("newRendered:", newRendered)
 
89
  return
90
  }
91
  })
92
+ }, 1000 * panel)
93
  }, [prompt, width, height])
94
 
95
 
 
98
  clearTimeout(timeoutRef.current)
99
 
100
  if (!renderedRef.current?.renderId || renderedRef.current?.status !== "pending") {
101
+ timeoutRef.current = setTimeout(checkStatus, 1500)
102
  return
103
  }
104
  try {
 
116
 
117
  if (newRendered.status === "pending") {
118
  // console.log("job not finished")
119
+ timeoutRef.current = setTimeout(checkStatus, 1500)
120
  } else {
121
  console.log("panel finished!")
122
  setGeneratingImages(panel, false)
123
  }
124
  } catch (err) {
125
  console.error(err)
126
+ timeoutRef.current = setTimeout(checkStatus, 1500)
127
  }
128
  })
129
  }
130
 
131
  useEffect(() => {
132
+ // console.log("starting timeout")
133
  clearTimeout(timeoutRef.current)
134
 
135
  // normally it should reply in < 1sec, but we could also use an interval
 
180
  `print:border-[1.5px] print:shadow-none`,
181
  )
182
 
183
+ const [newMask, setNewMask] = useState("")
184
+
185
+ useEffect(() => {
186
+ const transformMask = async () => {
187
+ if (rendered.maskUrl) {
188
+ const imgSrc = await writeIntoBubble(
189
+ rendered.maskUrl,
190
+ "LOREM IPSUM! Dolor sit amet.."
191
+ )
192
+ setNewMask(imgSrc)
193
+ }
194
+ }
195
+ transformMask()
196
+ }, [rendered.maskUrl])
197
+
198
+
199
  if (prompt && !rendered.assetUrl) {
200
  return (
201
  <div className={cn(
 
214
  { "grayscale": preset.color === "grayscale" },
215
  className
216
  )}>
217
+ {rendered.assetUrl && <img
218
+ src={rendered.assetUrl}
219
+ className=" w-full h-full object-cover"
220
+ alt={rendered.alt}
221
+ />}
222
+
223
+
224
 
225
  {/*<Bubble className="absolute top-4 left-4">
226
  Hello, world!
src/app/interface/progress/index.tsx CHANGED
@@ -24,7 +24,7 @@ export function Progress({
24
 
25
  // normally it takes 45, and we will try to go below,
26
  // but to be safe let's set the counter a 1 min
27
- const nbSeconds = 32 // 1 min
28
  const amountInPercent = 100 / (nbUpdatesPerSec * nbSeconds) // 0.333
29
 
30
  progressRef.current = Math.min(100, progressRef.current + amountInPercent)
 
24
 
25
  // normally it takes 45, and we will try to go below,
26
  // but to be safe let's set the counter a 1 min
27
+ const nbSeconds = 60 // 1 min
28
  const amountInPercent = 100 / (nbUpdatesPerSec * nbSeconds) // 0.333
29
 
30
  progressRef.current = Math.min(100, progressRef.current + amountInPercent)
src/app/interface/zoom/index.tsx CHANGED
@@ -15,7 +15,7 @@ export function Zoom() {
15
  `animation-all duration-300 ease-in-out`,
16
  isGeneratingStory ? `scale-0 opacity-0` : ``,
17
  )}>
18
- <div className="font-mono text-xs pb-1 text-stone-700">
19
  Zoom
20
  </div>
21
  <div className="w-2">
 
15
  `animation-all duration-300 ease-in-out`,
16
  isGeneratingStory ? `scale-0 opacity-0` : ``,
17
  )}>
18
+ <div className="font-mono text-xs pb-1 text-stone-700 bg-stone-50 rounded-full">
19
  Zoom
20
  </div>
21
  <div className="w-2">
src/app/layouts/index.tsx CHANGED
@@ -17,14 +17,14 @@ export function Layout1() {
17
  <div className="bg-zinc-100 row-span-2">
18
  <Panel
19
  panel={1}
20
- width={1024}
21
  height={1024}
22
  />
23
  </div>
24
  <div className="bg-gray-100 row-span-2 col-span-1">
25
  <Panel
26
  panel={2}
27
- width={1024}
28
  height={1024}
29
  />
30
  </div>
@@ -154,20 +154,20 @@ export function Layout5() {
154
  <Panel
155
  panel={0}
156
  width={768}
157
- height={768}
158
  />
159
  </div>
160
  <div className="bg-zinc-100 col-span-1 row-span-1">
161
  <Panel
162
  panel={1}
163
  width={768}
164
- height={768}
165
  />
166
  </div>
167
  <div className="bg-stone-100 row-span-2 col-span-1">
168
  <Panel
169
  panel={2}
170
- width={768}
171
  height={1024}
172
  />
173
  </div>
@@ -175,7 +175,7 @@ export function Layout5() {
175
  <Panel
176
  panel={3}
177
  width={1024}
178
- height={768}
179
  />
180
  </div>
181
  </Grid>
@@ -196,14 +196,14 @@ export function Layout6() {
196
  <Panel
197
  panel={1}
198
  width={768}
199
- height={768}
200
  />
201
  </div>
202
  <div className="bg-stone-100 row-span-1 col-span-1">
203
  <Panel
204
  panel={2}
205
  width={768}
206
- height={768}
207
  />
208
  </div>
209
  <div className="bg-slate-100 row-span-1 col-span-2">
@@ -218,11 +218,18 @@ export function Layout6() {
218
  }
219
 
220
  // export const layouts = { Layout1, Layout2, Layout3, Layout4, Layout5, Layout6 }
221
- export const layouts = { Layout1, Layout5, Layout6 }
 
 
 
 
222
 
223
- export type LayoutName = keyof typeof layouts
224
 
225
  export function getRandomLayoutName(): LayoutName {
226
- return pick(Object.keys(layouts) as LayoutName[]) as LayoutName
227
  }
228
 
 
 
 
 
17
  <div className="bg-zinc-100 row-span-2">
18
  <Panel
19
  panel={1}
20
+ width={768}
21
  height={1024}
22
  />
23
  </div>
24
  <div className="bg-gray-100 row-span-2 col-span-1">
25
  <Panel
26
  panel={2}
27
+ width={768}
28
  height={1024}
29
  />
30
  </div>
 
154
  <Panel
155
  panel={0}
156
  width={768}
157
+ height={1024}
158
  />
159
  </div>
160
  <div className="bg-zinc-100 col-span-1 row-span-1">
161
  <Panel
162
  panel={1}
163
  width={768}
164
+ height={1024}
165
  />
166
  </div>
167
  <div className="bg-stone-100 row-span-2 col-span-1">
168
  <Panel
169
  panel={2}
170
+ width={512}
171
  height={1024}
172
  />
173
  </div>
 
175
  <Panel
176
  panel={3}
177
  width={1024}
178
+ height={1024}
179
  />
180
  </div>
181
  </Grid>
 
196
  <Panel
197
  panel={1}
198
  width={768}
199
+ height={1024}
200
  />
201
  </div>
202
  <div className="bg-stone-100 row-span-1 col-span-1">
203
  <Panel
204
  panel={2}
205
  width={768}
206
+ height={1024}
207
  />
208
  </div>
209
  <div className="bg-slate-100 row-span-1 col-span-2">
 
218
  }
219
 
220
  // export const layouts = { Layout1, Layout2, Layout3, Layout4, Layout5, Layout6 }
221
+ export const allLayouts = {
222
+ Layout1,
223
+ Layout5,
224
+ Layout6
225
+ }
226
 
227
+ export type LayoutName = keyof typeof allLayouts
228
 
229
  export function getRandomLayoutName(): LayoutName {
230
+ return pick(Object.keys(allLayouts) as LayoutName[]) as LayoutName
231
  }
232
 
233
+ export function getRandomLayoutNames(): LayoutName[] {
234
+ return Object.keys(allLayouts).sort(() => Math.random() - 0.5) as LayoutName[]
235
+ }
src/app/layouts/new_layouts.tsx ADDED
File without changes
src/app/main.tsx CHANGED
@@ -1,18 +1,19 @@
1
  "use client"
2
 
3
- import { useEffect, useRef, useTransition } from "react"
4
  import { useSearchParams } from "next/navigation"
5
 
6
  import { PresetName, defaultPreset, getPreset } from "@/app/engine/presets"
7
 
8
  import { cn } from "@/lib/utils"
9
  import { TopMenu } from "./interface/top-menu"
10
- import { FontName, defaultFont, fontList, fonts } from "@/lib/fonts"
11
- import { getRandomLayoutName, layouts } from "./layouts"
12
  import { useStore } from "./store"
13
  import { Zoom } from "./interface/zoom"
14
  import { getStory } from "./queries/getStory"
15
  import { BottomBar } from "./interface/bottom-bar"
 
16
 
17
  export default function Main() {
18
  const [_isPending, startTransition] = useTransition()
@@ -34,8 +35,7 @@ export default function Main() {
34
  const prompt = useStore(state => state.prompt)
35
  const setPrompt = useStore(state => state.setPrompt)
36
 
37
- const layout = useStore(state => state.layout)
38
- const setLayout = useStore(state => state.setLayout)
39
 
40
  const setPanels = useStore(state => state.setPanels)
41
 
@@ -44,6 +44,8 @@ export default function Main() {
44
  const setPage = useStore(state => state.setPage)
45
  const pageRef = useRef<HTMLDivElement>(null)
46
 
 
 
47
  useEffect(() => {
48
  const element = pageRef.current
49
  if (!element) { return }
@@ -68,23 +70,28 @@ export default function Main() {
68
  if (!prompt) { return }
69
 
70
  startTransition(async () => {
71
-
72
  setGeneratingStory(true)
73
 
74
- const newLayout = getRandomLayoutName()
75
- console.log("using layout " + newLayout)
76
- setLayout(newLayout)
 
 
 
 
77
 
78
  try {
 
79
  const llmResponse = await getStory({ preset, prompt })
80
  console.log("response:", llmResponse)
81
 
82
- // TODO call the LLM here!
83
  const panelPromptPrefix = preset.imagePrompt(prompt).join(", ")
84
  console.log("panel prompt prefix:", panelPromptPrefix)
85
 
86
  const nbPanels = 4
87
  const newPanels: string[] = []
 
88
 
89
  for (let p = 0; p < nbPanels; p++) {
90
  const newPanel = [panelPromptPrefix, llmResponse[p] || ""]
@@ -95,13 +102,14 @@ export default function Main() {
95
  } catch (err) {
96
  console.error(err)
97
  } finally {
98
- setGeneratingStory(false)
 
 
 
99
  }
100
  })
101
  }, [prompt, preset?.label]) // important: we need to react to preset changes too
102
 
103
- const LayoutElement = (layouts as any)[layout]
104
-
105
  return (
106
  <div>
107
  <TopMenu />
@@ -112,35 +120,27 @@ export default function Main() {
112
  `print:pt-0 print:px-0 print:pl-0 print:pr-0`,
113
  fonts.actionman.className
114
  )}>
115
- <div className={cn(
116
- `flex flex-col w-full`,
117
- zoomLevel > 105 ? `items-start` : `items-center`
118
- )}>
 
 
119
  <div
120
- ref={pageRef}
121
  className={cn(
122
  `comic-page`,
123
- `flex flex-col items-center justify-start`,
124
-
125
- // we are trying to reach a "book" look
126
- // we are using aspect-[297/210] because it matches A4 (297mm x 210mm)
127
- // `aspect-[210/297]`,
128
- `aspect-[250/297]`,
129
-
130
- `transition-all duration-100 ease-in-out`,
131
- `border border-stone-200`,
132
- `shadow-2xl`,
133
- `print:shadow-none`,
134
- `print:border-0`,
135
- `print:width-screen`
136
  )}
137
  style={{
138
- width: `${zoomLevel}%`,
139
- padding: `${Math.round((zoomLevel / 100) * 16)}px`
140
- // marginLeft: `${zoomLevel > 100 ? `100`}`
141
- }}
142
- >
143
- <LayoutElement />
 
 
 
144
  </div>
145
  </div>
146
  </div>
@@ -157,11 +157,12 @@ export default function Main() {
157
  fonts.actionman.className
158
  )}>
159
  <div className={cn(
160
- `text-center text-lg text-stone-600 w-[70%]`,
161
  isGeneratingStory ? ``: `scale-0 opacity-0`,
162
  `transition-all duration-300 ease-in-out`,
163
  )}>
164
- Generating your story.. (hold tight)
 
165
  </div>
166
  </div>
167
  </div>
 
1
  "use client"
2
 
3
+ import { useEffect, useRef, useState, useTransition } from "react"
4
  import { useSearchParams } from "next/navigation"
5
 
6
  import { PresetName, defaultPreset, getPreset } from "@/app/engine/presets"
7
 
8
  import { cn } from "@/lib/utils"
9
  import { TopMenu } from "./interface/top-menu"
10
+ import { FontName, defaultFont, fonts } from "@/lib/fonts"
11
+ import { getRandomLayoutName } from "./layouts"
12
  import { useStore } from "./store"
13
  import { Zoom } from "./interface/zoom"
14
  import { getStory } from "./queries/getStory"
15
  import { BottomBar } from "./interface/bottom-bar"
16
+ import { Page } from "./interface/page"
17
 
18
  export default function Main() {
19
  const [_isPending, startTransition] = useTransition()
 
35
  const prompt = useStore(state => state.prompt)
36
  const setPrompt = useStore(state => state.setPrompt)
37
 
38
+ const setLayouts = useStore(state => state.setLayouts)
 
39
 
40
  const setPanels = useStore(state => state.setPanels)
41
 
 
44
  const setPage = useStore(state => state.setPage)
45
  const pageRef = useRef<HTMLDivElement>(null)
46
 
47
+ const [waitABitMore, setWaitABitMore] = useState(false)
48
+
49
  useEffect(() => {
50
  const element = pageRef.current
51
  if (!element) { return }
 
70
  if (!prompt) { return }
71
 
72
  startTransition(async () => {
73
+ setWaitABitMore(false)
74
  setGeneratingStory(true)
75
 
76
+ const newLayouts = [
77
+ getRandomLayoutName(),
78
+ getRandomLayoutName(),
79
+ ]
80
+
81
+ console.log("using layouts " + newLayouts)
82
+ setLayouts(newLayouts)
83
 
84
  try {
85
+
86
  const llmResponse = await getStory({ preset, prompt })
87
  console.log("response:", llmResponse)
88
 
 
89
  const panelPromptPrefix = preset.imagePrompt(prompt).join(", ")
90
  console.log("panel prompt prefix:", panelPromptPrefix)
91
 
92
  const nbPanels = 4
93
  const newPanels: string[] = []
94
+ setWaitABitMore(true)
95
 
96
  for (let p = 0; p < nbPanels; p++) {
97
  const newPanel = [panelPromptPrefix, llmResponse[p] || ""]
 
102
  } catch (err) {
103
  console.error(err)
104
  } finally {
105
+ setTimeout(() => {
106
+ setGeneratingStory(false)
107
+ setWaitABitMore(false)
108
+ }, 9000)
109
  }
110
  })
111
  }, [prompt, preset?.label]) // important: we need to react to preset changes too
112
 
 
 
113
  return (
114
  <div>
115
  <TopMenu />
 
120
  `print:pt-0 print:px-0 print:pl-0 print:pr-0`,
121
  fonts.actionman.className
122
  )}>
123
+ <div
124
+ ref={pageRef}
125
+ className={cn(
126
+ `flex flex-col w-full`,
127
+ zoomLevel > 105 ? `items-start` : `items-center`
128
+ )}>
129
  <div
 
130
  className={cn(
131
  `comic-page`,
132
+ `flex flex-col md:flex-row md:space-x-16 md:items-center md:justify-start`,
 
 
 
 
 
 
 
 
 
 
 
 
133
  )}
134
  style={{
135
+ width: `${zoomLevel}%`
136
+ }}>
137
+ <Page page={0} />
138
+
139
+ {/*
140
+ // we could support multiple pages here,
141
+ // but let's disable it for now
142
+ <Page page={1} />
143
+ */}
144
  </div>
145
  </div>
146
  </div>
 
157
  fonts.actionman.className
158
  )}>
159
  <div className={cn(
160
+ `text-center text-xl text-stone-600 w-[70%]`,
161
  isGeneratingStory ? ``: `scale-0 opacity-0`,
162
  `transition-all duration-300 ease-in-out`,
163
  )}>
164
+ {waitABitMore ? `Story is ready, but server is a bit busy!`: 'Generating a new story..'}<br/>
165
+ {waitABitMore ? `Please hold tight..` : ''}
166
  </div>
167
  </div>
168
  </div>
src/app/queries/getStory.ts CHANGED
@@ -19,10 +19,10 @@ export const getStory = async ({
19
  role: "system",
20
  content: [
21
  `You are a comic book author specialized in ${preset.llmPrompt}`,
22
- `Please generate detailed drawing instructions for the 4 panels of a new silent comic book page.`,
23
  `Give your response as a JSON array like this: \`Array<{ panel: number; caption: string}>\`.`,
24
  // `Give your response as Markdown bullet points.`,
25
- `Be brief in your caption don't add your own comments. Be straight to the point, and never reply things like "Sure, I can.." etc.`
26
  ].filter(item => item).join("\n")
27
  },
28
  {
 
19
  role: "system",
20
  content: [
21
  `You are a comic book author specialized in ${preset.llmPrompt}`,
22
+ `Please write detailed drawing instructions for the 5 panels of a new silent comic book page.`,
23
  `Give your response as a JSON array like this: \`Array<{ panel: number; caption: string}>\`.`,
24
  // `Give your response as Markdown bullet points.`,
25
+ `Be brief in your 5 captions, don't add your own comments. Be straight to the point, and never reply things like "Sure, I can.." etc.`
26
  ].filter(item => item).join("\n")
27
  },
28
  {
src/app/store/index.ts CHANGED
@@ -4,16 +4,17 @@ import { create } from "zustand"
4
 
5
  import { FontName } from "@/lib/fonts"
6
  import { Preset, getPreset } from "@/app/engine/presets"
7
- import { LayoutName, getRandomLayoutName } from "../layouts"
8
  import html2canvas from "html2canvas"
9
 
10
  export const useStore = create<{
11
  prompt: string
12
  font: FontName
13
  preset: Preset
 
14
  panels: string[]
15
  captions: Record<string, string>
16
- layout: LayoutName
17
  zoomLevel: number
18
  page: HTMLDivElement
19
  isGeneratingStory: boolean
@@ -24,7 +25,7 @@ export const useStore = create<{
24
  setFont: (font: FontName) => void
25
  setPreset: (preset: Preset) => void
26
  setPanels: (panels: string[]) => void
27
- setLayout: (layout: LayoutName) => void
28
  setCaption: (panelId: number, caption: string) => void
29
  setZoomLevel: (zoomLevel: number) => void
30
  setPage: (page: HTMLDivElement) => void
@@ -36,9 +37,10 @@ export const useStore = create<{
36
  prompt: "",
37
  font: "actionman",
38
  preset: getPreset("japanese_manga"),
 
39
  panels: [],
40
  captions: {},
41
- layout: getRandomLayoutName(),
42
  zoomLevel: 60,
43
  page: undefined as unknown as HTMLDivElement,
44
  isGeneratingStory: false,
@@ -50,7 +52,7 @@ export const useStore = create<{
50
  if (prompt === existingPrompt) { return }
51
  set({
52
  prompt,
53
- layout: getRandomLayoutName(),
54
  panels: [],
55
  captions: {},
56
  })
@@ -60,7 +62,7 @@ export const useStore = create<{
60
  if (font === existingFont) { return }
61
  set({
62
  font,
63
- layout: getRandomLayoutName(),
64
  panels: [],
65
  captions: {}
66
  })
@@ -70,7 +72,17 @@ export const useStore = create<{
70
  if (preset.label === existingPreset.label) { return }
71
  set({
72
  preset,
73
- layout: getRandomLayoutName(),
 
 
 
 
 
 
 
 
 
 
74
  panels: [],
75
  captions: {}
76
  })
@@ -84,7 +96,7 @@ export const useStore = create<{
84
  }
85
  })
86
  },
87
- setLayout: (layout: LayoutName) => set({ layout }),
88
  setZoomLevel: (zoomLevel: number) => set({ zoomLevel }),
89
  setPage: (page: HTMLDivElement) => {
90
  if (!page) { return }
 
4
 
5
  import { FontName } from "@/lib/fonts"
6
  import { Preset, getPreset } from "@/app/engine/presets"
7
+ import { LayoutName, getRandomLayoutNames } from "../layouts"
8
  import html2canvas from "html2canvas"
9
 
10
  export const useStore = create<{
11
  prompt: string
12
  font: FontName
13
  preset: Preset
14
+ nbFrames: number
15
  panels: string[]
16
  captions: Record<string, string>
17
+ layouts: LayoutName[]
18
  zoomLevel: number
19
  page: HTMLDivElement
20
  isGeneratingStory: boolean
 
25
  setFont: (font: FontName) => void
26
  setPreset: (preset: Preset) => void
27
  setPanels: (panels: string[]) => void
28
+ setLayouts: (layouts: LayoutName[]) => void
29
  setCaption: (panelId: number, caption: string) => void
30
  setZoomLevel: (zoomLevel: number) => void
31
  setPage: (page: HTMLDivElement) => void
 
37
  prompt: "",
38
  font: "actionman",
39
  preset: getPreset("japanese_manga"),
40
+ nbFrames: 1,
41
  panels: [],
42
  captions: {},
43
+ layouts: getRandomLayoutNames(),
44
  zoomLevel: 60,
45
  page: undefined as unknown as HTMLDivElement,
46
  isGeneratingStory: false,
 
52
  if (prompt === existingPrompt) { return }
53
  set({
54
  prompt,
55
+ layouts: getRandomLayoutNames(),
56
  panels: [],
57
  captions: {},
58
  })
 
62
  if (font === existingFont) { return }
63
  set({
64
  font,
65
+ layouts: getRandomLayoutNames(),
66
  panels: [],
67
  captions: {}
68
  })
 
72
  if (preset.label === existingPreset.label) { return }
73
  set({
74
  preset,
75
+ layouts: getRandomLayoutNames(),
76
+ panels: [],
77
+ captions: {}
78
+ })
79
+ },
80
+ setNbFrames: (nbFrames: number) => {
81
+ const existingNbFrames = get().nbFrames
82
+ if (nbFrames === existingNbFrames) { return }
83
+ set({
84
+ nbFrames,
85
+ layouts: getRandomLayoutNames(),
86
  panels: [],
87
  captions: {}
88
  })
 
96
  }
97
  })
98
  },
99
+ setLayouts: (layouts: LayoutName[]) => set({ layouts }),
100
  setZoomLevel: (zoomLevel: number) => set({ zoomLevel }),
101
  setPage: (page: HTMLDivElement) => {
102
  if (!page) { return }
src/lib/cropImage.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ async function cropImage(inputImage: string): Promise<{ croppedImage: string; x: number; y: number; width: number; height: number }> {
2
+ return new Promise((resolve, reject) => {
3
+ const img = new Image();
4
+ img.src = inputImage;
5
+ img.onload = () => {
6
+ const canvas = document.createElement('canvas');
7
+ const context = canvas.getContext('2d');
8
+ if (!context) {
9
+ reject("Context is null");
10
+ return;
11
+ }
12
+ canvas.width = img.width;
13
+ canvas.height = img.height;
14
+ context.drawImage(img, 0, 0, img.width, img.height);
15
+ const imageData = context.getImageData(0, 0, img.width, img.height);
16
+ const data = imageData.data;
17
+ let minX = img.width, minY = img.height, maxX = 0, maxY = 0;
18
+
19
+ for (let y = 0; y < img.height; y++) {
20
+ for (let x = 0; x < img.width; x++) {
21
+ const i = (y * 4) * img.width + x * 4;
22
+ const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
23
+ if (avg < 255) {
24
+ minX = Math.min(minX, x);
25
+ minY = Math.min(minY, y);
26
+ maxX = Math.max(maxX, x);
27
+ maxY = Math.max(maxY, y);
28
+ }
29
+ }
30
+ }
31
+
32
+ const width = maxX - minX;
33
+ const height = maxY - minY;
34
+ const croppedCanvas = document.createElement('canvas');
35
+ croppedCanvas.width = width;
36
+ croppedCanvas.height = height;
37
+ const croppedCtx = croppedCanvas.getContext('2d');
38
+ if (!croppedCtx) {
39
+ reject("croppedCtx is null");
40
+ return;
41
+ }
42
+ croppedCtx.drawImage(canvas, minX, minY, width, height, 0, 0, width, height);
43
+ resolve({
44
+ croppedImage: croppedCanvas.toDataURL(),
45
+ x: minX,
46
+ y: minY,
47
+ width,
48
+ height
49
+ });
50
+ };
51
+ img.onerror = reject;
52
+ });
53
+ }
src/lib/loadImage.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export async function loadImage(image: string): Promise<HTMLImageElement> {
2
+ const img = new Image();
3
+ img.src = image;
4
+
5
+ const imgOnLoad = () => {
6
+ return new Promise<HTMLImageElement>((resolve, reject) => {
7
+ img.onload = () => { resolve(img) };
8
+ img.onerror = (err) => { reject(err) };
9
+ })
10
+ };
11
+
12
+ const loadImg = await imgOnLoad();
13
+ return loadImg
14
+ }
src/lib/replaceNonWhiteWithTransparent.ts ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function replaceNonWhiteWithTransparent(imageBase64: string): Promise<string> {
2
+ return new Promise((resolve, reject) => {
3
+ const img = new Image();
4
+ img.onload = () => {
5
+ const canvas = document.createElement('canvas');
6
+ const ctx = canvas.getContext('2d');
7
+ if (!ctx) {
8
+ reject('Unable to get canvas context');
9
+ return;
10
+ }
11
+
12
+ const ratio = window.devicePixelRatio || 1;
13
+ canvas.width = img.width * ratio;
14
+ canvas.height = img.height * ratio;
15
+ ctx.scale(ratio, ratio);
16
+
17
+ ctx.drawImage(img, 0, 0);
18
+
19
+ const imageData = ctx.getImageData(0, 0, img.width, img.height);
20
+ const data = imageData.data;
21
+ console.log("ok")
22
+
23
+ for (let i = 0; i < data.length; i += 4) {
24
+ if (data[i] === 255 && data[i + 1] === 255 && data[i + 2] === 255) {
25
+ // Change white (also shades of grays) pixels to black
26
+ data[i] = 0;
27
+ data[i + 1] = 0;
28
+ data[i + 2] = 0;
29
+ } else {
30
+ // Change all other pixels to transparent
31
+ data[i + 3] = 0;
32
+ }
33
+ }
34
+
35
+ ctx.putImageData(imageData, 0, 0);
36
+
37
+ resolve(canvas.toDataURL());
38
+ };
39
+
40
+ img.onerror = (err) => {
41
+ reject(err);
42
+ };
43
+
44
+ img.src = imageBase64;
45
+ });
46
+ }
src/lib/replaceWhiteWithTransparent.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function replaceWhiteWithTransparent(imageBase64: string): Promise<string> {
2
+ return new Promise((resolve, reject) => {
3
+ const img = new Image();
4
+ img.onload = () => {
5
+ const canvas = document.createElement('canvas');
6
+ canvas.width = img.width;
7
+ canvas.height = img.height;
8
+
9
+ const ctx = canvas.getContext('2d');
10
+ if (!ctx) {
11
+ reject('Unable to get canvas 2D context');
12
+ return;
13
+ }
14
+
15
+ ctx.drawImage(img, 0, 0);
16
+
17
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
18
+ const data = imageData.data;
19
+
20
+ for (let i = 0; i < data.length; i += 4) {
21
+ if (data[i] === 255 && data[i + 1] === 255 && data[i + 2] === 255) {
22
+ data[i + 3] = 0;
23
+ }
24
+ }
25
+
26
+ ctx.putImageData(imageData, 0, 0);
27
+
28
+ resolve(canvas.toDataURL());
29
+ };
30
+
31
+ img.onerror = (err) => {
32
+ reject(err);
33
+ };
34
+
35
+ img.src = imageBase64;
36
+ });
37
+ }
src/lib/writeIntoBubble.ts ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ I have a PNG image which contains a colored shape (roughly in the shape of a speech bubble), surrounded by white
3
+
4
+ Please write a TypeScript function (it should work in the browser) to:
5
+
6
+ 1. replace all the white pixels with a transparent PNG pixel
7
+ 2. replace all the colored pixels with a white pixel
8
+ 3. write some input text into the colored shape
9
+ 4. Make sure line returns are handled
10
+ 5. It should have some padding (eg. 20px)
11
+ 6. use Comic Sans MS
12
+
13
+ You can use the canvas for your operation. The signature should be something like:
14
+
15
+ - Please adjust the font size, based on the available number of pixels inside the bubble, taking some margin into account.
16
+ - The text should not be below 8px
17
+ - If there is not enough room to display it without going outside the shape, then crop the text.
18
+ - in other words, NEVER write outside the shape!
19
+
20
+ The function should be something like:
21
+
22
+ writeIntoBubble(image: string, text: string): Promise<string>
23
+ */
24
+
25
+ export async function writeIntoBubble(image: string, text: string): Promise<string> {
26
+ const padding = 20; // Pixels
27
+ return new Promise((resolve, reject) => {
28
+ const img = new Image();
29
+ img.onload = () => {
30
+ const physicalWidth = img.width;
31
+ const physicalHeight = img.height;
32
+ const canvas = document.createElement('canvas');
33
+ const ctx = canvas.getContext('2d');
34
+ if (!ctx) {
35
+ reject('Unable to get canvas context');
36
+ return;
37
+ }
38
+ canvas.width = physicalWidth;
39
+ canvas.height = physicalHeight;
40
+ ctx.drawImage(img, 0, 0, physicalWidth, physicalHeight);
41
+
42
+ const imageData = ctx.getImageData(0, 0, physicalWidth, physicalHeight);
43
+ const data = imageData.data;
44
+
45
+ let minX = physicalWidth, minY = physicalHeight, maxX = 0, maxY = 0;
46
+
47
+ for (let y = 0; y < physicalHeight; y++) {
48
+ for (let x = 0; x < physicalWidth; x++) {
49
+ const i = (y * physicalWidth + x) * 4;
50
+ if (data[i] !== 255 || data[i + 1] !== 255 || data[i + 2] !== 255) {
51
+ minX = Math.min(minX, x);
52
+ minY = Math.min(minY, y);
53
+ maxX = Math.max(maxX, x);
54
+ maxY = Math.max(maxY, y);
55
+ data[i] = data[i + 1] = data[i + 2] = 255;
56
+ data[i + 3] = 255;
57
+ } else {
58
+ data[i + 3] = 0;
59
+ }
60
+ }
61
+ }
62
+
63
+ ctx.putImageData(imageData, 0, 0);
64
+
65
+ ctx.save();
66
+ ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transforms to handle padding correctly
67
+
68
+ const textX = minX + padding;
69
+ const textY = minY + padding;
70
+ const textWidth = (maxX - minX) - 2 * padding;
71
+ const textHeight = (maxY - minY) - 2 * padding;
72
+
73
+ ctx.restore();
74
+
75
+ ctx.rect(textX, textY, textWidth, textHeight);
76
+ ctx.clip(); // Clip outside of the region
77
+
78
+ let fontSize = 20; // Start with a large size
79
+ let lines = [];
80
+ do {
81
+ ctx.font = `${fontSize}px Comic Sans MS`;
82
+ lines = wrapText(ctx, text, textWidth);
83
+ fontSize -= 2; // Reduce size and try again if text doesn't fit
84
+ } while(lines.length > textHeight / fontSize && fontSize > 8);
85
+ ctx.font = `${fontSize}px Comic Sans MS`;
86
+
87
+ lines.forEach((line, i) => ctx.fillText(line, textX, textY + padding + i * fontSize));
88
+
89
+ resolve(canvas.toDataURL());
90
+ };
91
+ img.onerror = reject;
92
+ img.src = image;
93
+ });
94
+ }
95
+
96
+ // Function to wrap text into lines that fit inside a specified width
97
+ function wrapText(context: CanvasRenderingContext2D, text: string, maxWidth: number): string[] {
98
+ const words = text.split(' ');
99
+ const lines = [];
100
+ let line = '';
101
+
102
+ for (let n = 0; n < words.length; n++) {
103
+ const testLine = line + words[n] + ' ';
104
+ const metrics = context.measureText(testLine);
105
+ const testWidth = metrics.width;
106
+ if (testWidth > maxWidth && n > 0) {
107
+ lines.push(line);
108
+ line = words[n] + ' ';
109
+ } else {
110
+ line = testLine;
111
+ }
112
+ }
113
+ lines.push(line);
114
+ return lines;
115
+ }
src/lib/writeIntoBubbles.ts ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { loadImage } from "./loadImage"
2
+ import { writeIntoBubble } from "./writeIntoBubble"
3
+
4
+ export async function writeIntoBubbles(image: string, texts: string[]): Promise<string> {
5
+ const loadImg = await loadImage(image);
6
+
7
+ const canvas = document.createElement('canvas');
8
+ const context = canvas.getContext('2d');
9
+
10
+ canvas.width = loadImg.width;
11
+ canvas.height = loadImg.height;
12
+ context?.drawImage(loadImg, 0, 0, loadImg.width, loadImg.height);
13
+
14
+ const untouchedImageData = context?.getImageData(0, 0, loadImg.width, loadImg.height);
15
+ if (!untouchedImageData) {
16
+ throw new Error("untouchedImageData is invalid")
17
+ }
18
+ const colorSet = new Set<string>(); // This is the unique color container
19
+
20
+ for(let i = 0; i < untouchedImageData?.data.length; i += 4){
21
+ const r = untouchedImageData?.data[i];
22
+ const g = untouchedImageData?.data[i+1];
23
+ const b = untouchedImageData?.data[i+2];
24
+ const colorString = `rgb(${r},${g},${b})`;
25
+
26
+ if(!colorSet.has(colorString)){
27
+ colorSet.add(colorString);
28
+ var newCanvas = document.createElement('canvas');
29
+ newCanvas.width = loadImg.width;
30
+ newCanvas.height = loadImg.height;
31
+
32
+ var newContext = newCanvas.getContext('2d');
33
+ newContext?.drawImage(loadImg, 0, 0, loadImg.width, loadImg.height);
34
+ var newImageData = newContext?.getImageData(0, 0, loadImg.width, loadImg.height);
35
+ if (!newImageData) {
36
+ throw new Error("newImageData is invalid")
37
+ }
38
+
39
+ for(let j = 0; j < newImageData?.data.length; j += 4){
40
+ const _r = newImageData?.data[j];
41
+ const _g = newImageData?.data[j+1];
42
+ const _b = newImageData?.data[j+2];
43
+ const _colorString = `rgb(${_r},${_g},${_b})`;
44
+
45
+ if(_colorString !== colorString){
46
+ newImageData?.data.set([0,0,0,0], j);
47
+ }
48
+ }
49
+
50
+ newContext?.putImageData(newImageData as ImageData, 0, 0);
51
+
52
+ let imageBase64 = newCanvas.toDataURL();
53
+
54
+ if(texts.length > 0){
55
+ let text = texts.shift() as string;
56
+ if (imageBase64 != '') {
57
+ const processedBase64 = await writeIntoBubble(imageBase64, text);
58
+ const newImg = await loadImage(processedBase64);
59
+ context?.drawImage(newImg, 0, 0, loadImg.width, loadImg.height);
60
+ }
61
+ }
62
+ }
63
+ }
64
+ return canvas.toDataURL();
65
+ }