Spaces:
Sleeping
Sleeping
Commit
•
421fbba
1
Parent(s):
53fde26
add a new experimental feature
Browse files- package-lock.json +26 -0
- package.json +1 -0
- src/app/interface/about/index.tsx +2 -2
- src/app/interface/bottom-bar/bottom-bar.tsx +34 -4
- src/app/interface/panel/index.tsx +16 -1
- src/app/interface/share/index.tsx +2 -2
- src/app/interface/top-menu/index.tsx +3 -0
- src/app/main.tsx +7 -0
- src/app/store/index.ts +136 -7
- src/lib/fileToBase64.ts +8 -0
- src/lib/putTextInInput.ts +18 -0
- src/types.ts +1 -0
package-lock.json
CHANGED
@@ -69,6 +69,7 @@
|
|
69 |
"tailwindcss-animate": "^1.0.6",
|
70 |
"ts-node": "^10.9.1",
|
71 |
"typescript": "^5.4.5",
|
|
|
72 |
"usehooks-ts": "2.9.1",
|
73 |
"uuid": "^9.0.0",
|
74 |
"zustand": "^4.4.1"
|
@@ -4320,6 +4321,17 @@
|
|
4320 |
"node": "^10.12.0 || >=12.0.0"
|
4321 |
}
|
4322 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4323 |
"node_modules/fill-range": {
|
4324 |
"version": "7.0.1",
|
4325 |
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
@@ -9992,6 +10004,20 @@
|
|
9992 |
}
|
9993 |
}
|
9994 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9995 |
"node_modules/use-sidecar": {
|
9996 |
"version": "1.1.2",
|
9997 |
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
|
|
|
69 |
"tailwindcss-animate": "^1.0.6",
|
70 |
"ts-node": "^10.9.1",
|
71 |
"typescript": "^5.4.5",
|
72 |
+
"use-file-picker": "^2.1.2",
|
73 |
"usehooks-ts": "2.9.1",
|
74 |
"uuid": "^9.0.0",
|
75 |
"zustand": "^4.4.1"
|
|
|
4321 |
"node": "^10.12.0 || >=12.0.0"
|
4322 |
}
|
4323 |
},
|
4324 |
+
"node_modules/file-selector": {
|
4325 |
+
"version": "0.2.4",
|
4326 |
+
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.2.4.tgz",
|
4327 |
+
"integrity": "sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==",
|
4328 |
+
"dependencies": {
|
4329 |
+
"tslib": "^2.0.3"
|
4330 |
+
},
|
4331 |
+
"engines": {
|
4332 |
+
"node": ">= 10"
|
4333 |
+
}
|
4334 |
+
},
|
4335 |
"node_modules/fill-range": {
|
4336 |
"version": "7.0.1",
|
4337 |
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
|
|
10004 |
}
|
10005 |
}
|
10006 |
},
|
10007 |
+
"node_modules/use-file-picker": {
|
10008 |
+
"version": "2.1.2",
|
10009 |
+
"resolved": "https://registry.npmjs.org/use-file-picker/-/use-file-picker-2.1.2.tgz",
|
10010 |
+
"integrity": "sha512-ZEIzRi1wXeIXDWr5i55gRBVER8rTkSGskDUY94bciTTAZJHlBnOTRLL/LDYjgz6d+US3yELHnRvtBhLxFGtB0A==",
|
10011 |
+
"dependencies": {
|
10012 |
+
"file-selector": "0.2.4"
|
10013 |
+
},
|
10014 |
+
"engines": {
|
10015 |
+
"node": ">=12"
|
10016 |
+
},
|
10017 |
+
"peerDependencies": {
|
10018 |
+
"react": ">=16"
|
10019 |
+
}
|
10020 |
+
},
|
10021 |
"node_modules/use-sidecar": {
|
10022 |
"version": "1.1.2",
|
10023 |
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
|
package.json
CHANGED
@@ -70,6 +70,7 @@
|
|
70 |
"tailwindcss-animate": "^1.0.6",
|
71 |
"ts-node": "^10.9.1",
|
72 |
"typescript": "^5.4.5",
|
|
|
73 |
"usehooks-ts": "2.9.1",
|
74 |
"uuid": "^9.0.0",
|
75 |
"zustand": "^4.4.1"
|
|
|
70 |
"tailwindcss-animate": "^1.0.6",
|
71 |
"ts-node": "^10.9.1",
|
72 |
"typescript": "^5.4.5",
|
73 |
+
"use-file-picker": "^2.1.2",
|
74 |
"usehooks-ts": "2.9.1",
|
75 |
"uuid": "^9.0.0",
|
76 |
"zustand": "^4.4.1"
|
src/app/interface/about/index.tsx
CHANGED
@@ -8,8 +8,8 @@ import { Login } from "../login"
|
|
8 |
const APP_NAME = `AI Comic Factory`
|
9 |
const APP_DOMAIN = `aicomicfactory.app`
|
10 |
const APP_URL = `https://aicomicfactory.app`
|
11 |
-
const APP_VERSION = `1.
|
12 |
-
const APP_RELEASE_DATE = `
|
13 |
|
14 |
const ExternalLink = ({ url, children }: { url: string; children: ReactNode }) => {
|
15 |
return (
|
|
|
8 |
const APP_NAME = `AI Comic Factory`
|
9 |
const APP_DOMAIN = `aicomicfactory.app`
|
10 |
const APP_URL = `https://aicomicfactory.app`
|
11 |
+
const APP_VERSION = `1.4`
|
12 |
+
const APP_RELEASE_DATE = `May 2024`
|
13 |
|
14 |
const ExternalLink = ({ url, children }: { url: string; children: ReactNode }) => {
|
15 |
return (
|
src/app/interface/bottom-bar/bottom-bar.tsx
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
import { startTransition, useEffect, useState } from "react"
|
|
|
2 |
|
3 |
import { useStore } from "@/app/store"
|
4 |
import { Button } from "@/components/ui/button"
|
@@ -14,6 +15,7 @@ import { useLocalStorage } from "usehooks-ts"
|
|
14 |
import { localStorageKeys } from "../settings-dialog/localStorageKeys"
|
15 |
import { defaultSettings } from "../settings-dialog/defaultSettings"
|
16 |
import { getParam } from "@/lib/getParam"
|
|
|
17 |
|
18 |
function BottomBar() {
|
19 |
// deprecated, as HTML-to-bitmap didn't work that well for us
|
@@ -32,12 +34,15 @@ function BottomBar() {
|
|
32 |
const allStatus = Object.values(panelGenerationStatus)
|
33 |
const remainingImages = allStatus.reduce((acc, s) => (acc + (s ? 1 : 0)), 0)
|
34 |
|
|
|
|
|
35 |
const upscaleQueue = useStore(s => s.upscaleQueue)
|
36 |
const renderedScenes = useStore(s => s.renderedScenes)
|
37 |
const removeFromUpscaleQueue = useStore(s => s.removeFromUpscaleQueue)
|
38 |
const setRendered = useStore(s => s.setRendered)
|
39 |
const [isUpscaling, setUpscaling] = useState(false)
|
40 |
|
|
|
41 |
const downloadClap = useStore(s => s.downloadClap)
|
42 |
|
43 |
const [hasGeneratedAtLeastOnce, setHasGeneratedAtLeastOnce] = useLocalStorage<boolean>(
|
@@ -87,6 +92,27 @@ function BottomBar() {
|
|
87 |
}
|
88 |
}, [hasFinishedGeneratingImages, hasGeneratedAtLeastOnce])
|
89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
90 |
return (
|
91 |
<div className={cn(
|
92 |
`print:hidden`,
|
@@ -152,21 +178,25 @@ function BottomBar() {
|
|
152 |
</Button>
|
153 |
</div>
|
154 |
*/}
|
|
|
|
|
|
|
|
|
155 |
{canSeeBetaFeatures ? <Button
|
156 |
onClick={downloadClap}
|
157 |
-
disabled={!prompt?.length || remainingImages > 0}
|
158 |
>
|
159 |
-
{remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} ⌛` : `Save
|
160 |
</Button> : null}
|
161 |
<Button
|
162 |
onClick={handlePrint}
|
163 |
disabled={!prompt?.length}
|
164 |
>
|
165 |
<span className="hidden md:inline">{
|
166 |
-
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} panels ⌛` : `
|
167 |
}</span>
|
168 |
<span className="inline md:hidden">{
|
169 |
-
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} ⌛` : `
|
170 |
}</span>
|
171 |
</Button>
|
172 |
<Share />
|
|
|
1 |
import { startTransition, useEffect, useState } from "react"
|
2 |
+
import { useFilePicker } from 'use-file-picker'
|
3 |
|
4 |
import { useStore } from "@/app/store"
|
5 |
import { Button } from "@/components/ui/button"
|
|
|
15 |
import { localStorageKeys } from "../settings-dialog/localStorageKeys"
|
16 |
import { defaultSettings } from "../settings-dialog/defaultSettings"
|
17 |
import { getParam } from "@/lib/getParam"
|
18 |
+
import { Input } from "@/components/ui/input"
|
19 |
|
20 |
function BottomBar() {
|
21 |
// deprecated, as HTML-to-bitmap didn't work that well for us
|
|
|
34 |
const allStatus = Object.values(panelGenerationStatus)
|
35 |
const remainingImages = allStatus.reduce((acc, s) => (acc + (s ? 1 : 0)), 0)
|
36 |
|
37 |
+
const currentClap = useStore(s => s.currentClap)
|
38 |
+
|
39 |
const upscaleQueue = useStore(s => s.upscaleQueue)
|
40 |
const renderedScenes = useStore(s => s.renderedScenes)
|
41 |
const removeFromUpscaleQueue = useStore(s => s.removeFromUpscaleQueue)
|
42 |
const setRendered = useStore(s => s.setRendered)
|
43 |
const [isUpscaling, setUpscaling] = useState(false)
|
44 |
|
45 |
+
const loadClap = useStore(s => s.loadClap)
|
46 |
const downloadClap = useStore(s => s.downloadClap)
|
47 |
|
48 |
const [hasGeneratedAtLeastOnce, setHasGeneratedAtLeastOnce] = useLocalStorage<boolean>(
|
|
|
92 |
}
|
93 |
}, [hasFinishedGeneratingImages, hasGeneratedAtLeastOnce])
|
94 |
|
95 |
+
const { openFilePicker, filesContent } = useFilePicker({
|
96 |
+
accept: '.clap',
|
97 |
+
readAs: "ArrayBuffer"
|
98 |
+
})
|
99 |
+
const fileData = filesContent[0]
|
100 |
+
|
101 |
+
useEffect(() => {
|
102 |
+
const fn = async () => {
|
103 |
+
if (fileData?.name) {
|
104 |
+
try {
|
105 |
+
const blob = new Blob([fileData.content])
|
106 |
+
await loadClap(blob)
|
107 |
+
} catch (err) {
|
108 |
+
console.error("failed to load the Clap file:", err)
|
109 |
+
}
|
110 |
+
}
|
111 |
+
}
|
112 |
+
fn()
|
113 |
+
}, [fileData?.name])
|
114 |
+
|
115 |
+
|
116 |
return (
|
117 |
<div className={cn(
|
118 |
`print:hidden`,
|
|
|
178 |
</Button>
|
179 |
</div>
|
180 |
*/}
|
181 |
+
{canSeeBetaFeatures ? <Button
|
182 |
+
onClick={openFilePicker}
|
183 |
+
disabled={remainingImages > 0}
|
184 |
+
>Load</Button> : null}
|
185 |
{canSeeBetaFeatures ? <Button
|
186 |
onClick={downloadClap}
|
187 |
+
disabled={!prompt?.length || remainingImages > 0 || !currentClap}
|
188 |
>
|
189 |
+
{remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} ⌛` : `Save`}
|
190 |
</Button> : null}
|
191 |
<Button
|
192 |
onClick={handlePrint}
|
193 |
disabled={!prompt?.length}
|
194 |
>
|
195 |
<span className="hidden md:inline">{
|
196 |
+
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} panels ⌛` : `Get PDF`
|
197 |
}</span>
|
198 |
<span className="inline md:hidden">{
|
199 |
+
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} ⌛` : `PDF`
|
200 |
}</span>
|
201 |
</Button>
|
202 |
<Share />
|
src/app/interface/panel/index.tsx
CHANGED
@@ -286,6 +286,15 @@ export function Panel({
|
|
286 |
useEffect(() => {
|
287 |
if (!prompt.length) { return }
|
288 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
289 |
startImageGeneration({ prompt, width, height, nbFrames, revision })
|
290 |
|
291 |
clearTimeout(timeoutRef.current)
|
@@ -456,7 +465,13 @@ export function Panel({
|
|
456 |
height={height}
|
457 |
alt={rendered.alt}
|
458 |
className={cn(
|
459 |
-
`comic-panel w-full h-full
|
|
|
|
|
|
|
|
|
|
|
|
|
460 |
// showCaptions ? `-mt-11` : ''
|
461 |
)}
|
462 |
/>}
|
|
|
286 |
useEffect(() => {
|
287 |
if (!prompt.length) { return }
|
288 |
|
289 |
+
const renderedScene: RenderedScene | undefined = useStore.getState().renderedScenes[panelIndex]
|
290 |
+
|
291 |
+
// I'm trying to find a rule to handle the case were we load a .clap file
|
292 |
+
// I think we should trash all the Panel objects for this to work properly
|
293 |
+
if (renderedScene && renderedScene.status === "pregenerated" && renderedScene.assetUrl) {
|
294 |
+
console.log(`loading a pre-generated panel..`)
|
295 |
+
return
|
296 |
+
}
|
297 |
+
|
298 |
startImageGeneration({ prompt, width, height, nbFrames, revision })
|
299 |
|
300 |
clearTimeout(timeoutRef.current)
|
|
|
465 |
height={height}
|
466 |
alt={rendered.alt}
|
467 |
className={cn(
|
468 |
+
`comic-panel w-full h-full`,
|
469 |
+
`object-cover`,
|
470 |
+
|
471 |
+
// I think we can remove this to improve compatibility,
|
472 |
+
// in case the generate image isn't exactly the same size
|
473 |
+
// `max-w-max`,
|
474 |
+
|
475 |
// showCaptions ? `-mt-11` : ''
|
476 |
)}
|
477 |
/>}
|
src/app/interface/share/index.tsx
CHANGED
@@ -119,10 +119,10 @@ ${comicFileMd}`;
|
|
119 |
disabled={!prompt?.length}
|
120 |
>
|
121 |
<span className="hidden md:inline">{
|
122 |
-
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} panels ⌛` : `
|
123 |
}</span>
|
124 |
<span className="inline md:hidden">{
|
125 |
-
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} ⌛` : `
|
126 |
}</span>
|
127 |
</Button>
|
128 |
</p>
|
|
|
119 |
disabled={!prompt?.length}
|
120 |
>
|
121 |
<span className="hidden md:inline">{
|
122 |
+
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} panels ⌛` : `Get PDF`
|
123 |
}</span>
|
124 |
<span className="inline md:hidden">{
|
125 |
+
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} ⌛` : `PDF`
|
126 |
}</span>
|
127 |
</Button>
|
128 |
</p>
|
src/app/interface/top-menu/index.tsx
CHANGED
@@ -79,6 +79,7 @@ export function TopMenu() {
|
|
79 |
requestedStoryPrompt
|
80 |
)
|
81 |
|
|
|
82 |
const [draftPromptA, setDraftPromptA] = useState(lastDraftPromptA)
|
83 |
const [draftPromptB, setDraftPromptB] = useState(lastDraftPromptB)
|
84 |
const draftPrompt = `${draftPromptA}||${draftPromptB}`
|
@@ -242,6 +243,7 @@ export function TopMenu() {
|
|
242 |
<div className="flex flex-row flex-grow w-full">
|
243 |
<div className="flex flex-row flex-grow w-full">
|
244 |
<Input
|
|
|
245 |
placeholder="1. Story (eg. detective dog)"
|
246 |
className={cn(
|
247 |
`w-1/2 rounded-r-none`,
|
@@ -260,6 +262,7 @@ export function TopMenu() {
|
|
260 |
value={draftPromptB}
|
261 |
/>
|
262 |
<Input
|
|
|
263 |
placeholder="2. Style (eg 'rain, shiba')"
|
264 |
className={cn(
|
265 |
`w-1/2`,
|
|
|
79 |
requestedStoryPrompt
|
80 |
)
|
81 |
|
82 |
+
// TODO should be in the store
|
83 |
const [draftPromptA, setDraftPromptA] = useState(lastDraftPromptA)
|
84 |
const [draftPromptB, setDraftPromptB] = useState(lastDraftPromptB)
|
85 |
const draftPrompt = `${draftPromptA}||${draftPromptB}`
|
|
|
243 |
<div className="flex flex-row flex-grow w-full">
|
244 |
<div className="flex flex-row flex-grow w-full">
|
245 |
<Input
|
246 |
+
id="top-menu-input-story-prompt"
|
247 |
placeholder="1. Story (eg. detective dog)"
|
248 |
className={cn(
|
249 |
`w-1/2 rounded-r-none`,
|
|
|
262 |
value={draftPromptB}
|
263 |
/>
|
264 |
<Input
|
265 |
+
id="top-menu-input-style-prompt"
|
266 |
placeholder="2. Style (eg 'rain, shiba')"
|
267 |
className={cn(
|
268 |
`w-1/2`,
|
src/app/main.tsx
CHANGED
@@ -121,6 +121,13 @@ export default function Main() {
|
|
121 |
// console.log(`main.tsx: asked to re-generate!!`)
|
122 |
if (!prompt) { return }
|
123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
124 |
// if the prompt or preset changed, we clear the cache
|
125 |
// this part is important, otherwise when trying to change the prompt
|
126 |
// we wouldn't still have remnants of the previous comic
|
|
|
121 |
// console.log(`main.tsx: asked to re-generate!!`)
|
122 |
if (!prompt) { return }
|
123 |
|
124 |
+
// a quick and dirty hack to skip prompt regeneration,
|
125 |
+
// unless the prompt has really changed
|
126 |
+
if (prompt === useStore.getState().currentClap?.meta.description) {
|
127 |
+
console.log(`loading a pre-generated comic, so skipping prompt regeneration..`)
|
128 |
+
return
|
129 |
+
}
|
130 |
+
|
131 |
// if the prompt or preset changed, we clear the cache
|
132 |
// this part is important, otherwise when trying to change the prompt
|
133 |
// we wouldn't still have remnants of the previous comic
|
src/app/store/index.ts
CHANGED
@@ -1,14 +1,16 @@
|
|
1 |
"use client"
|
2 |
|
3 |
import { create } from "zustand"
|
4 |
-
import { ClapProject, newClap, newSegment, serializeClap } from "@aitube/clap"
|
5 |
|
6 |
import { FontName } from "@/lib/fonts"
|
7 |
import { Preset, PresetName, defaultPreset, getPreset, getRandomPreset } from "@/app/engine/presets"
|
8 |
import { RenderedScene } from "@/types"
|
9 |
-
import { LayoutName, defaultLayout, getRandomLayoutName } from "../layouts"
|
10 |
import { getParam } from "@/lib/getParam"
|
11 |
|
|
|
|
|
|
|
12 |
export const useStore = create<{
|
13 |
prompt: string
|
14 |
font: FontName
|
@@ -71,9 +73,17 @@ export const useStore = create<{
|
|
71 |
// setPage: (page: HTMLDivElement) => void
|
72 |
|
73 |
generate: (prompt: string, presetName: PresetName, layoutName: LayoutName) => void
|
74 |
-
|
75 |
convertComicToClap: () => Promise<ClapProject>
|
76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
77 |
downloadClap: () => Promise<void>
|
78 |
}>((set, get) => ({
|
79 |
prompt:
|
@@ -406,6 +416,7 @@ export const useStore = create<{
|
|
406 |
layouts,
|
407 |
})
|
408 |
},
|
|
|
409 |
convertComicToClap: async (): Promise<ClapProject> => {
|
410 |
const {
|
411 |
currentNbPanels,
|
@@ -497,8 +508,120 @@ export const useStore = create<{
|
|
497 |
return clap
|
498 |
},
|
499 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
500 |
downloadClap: async () => {
|
501 |
-
const { convertComicToClap } = get()
|
502 |
|
503 |
const currentClap = await convertComicToClap()
|
504 |
|
@@ -513,7 +636,13 @@ export const useStore = create<{
|
|
513 |
const anchor = document.createElement("a")
|
514 |
anchor.href = objectUrl
|
515 |
|
516 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
517 |
|
518 |
document.body.appendChild(anchor) // Append to the body (could be removed once clicked)
|
519 |
anchor.click() // Trigger the download
|
@@ -521,5 +650,5 @@ export const useStore = create<{
|
|
521 |
// Cleanup: revoke the object URL and remove the anchor element
|
522 |
URL.revokeObjectURL(objectUrl)
|
523 |
document.body.removeChild(anchor)
|
524 |
-
}
|
525 |
}))
|
|
|
1 |
"use client"
|
2 |
|
3 |
import { create } from "zustand"
|
4 |
+
import { ClapProject, ClapSegment, ClapSegmentFilteringMode, filterSegments, newClap, newSegment, parseClap, serializeClap } from "@aitube/clap"
|
5 |
|
6 |
import { FontName } from "@/lib/fonts"
|
7 |
import { Preset, PresetName, defaultPreset, getPreset, getRandomPreset } from "@/app/engine/presets"
|
8 |
import { RenderedScene } from "@/types"
|
|
|
9 |
import { getParam } from "@/lib/getParam"
|
10 |
|
11 |
+
import { LayoutName, defaultLayout, getRandomLayoutName } from "../layouts"
|
12 |
+
import { putTextInInput } from "@/lib/putTextInInput"
|
13 |
+
|
14 |
export const useStore = create<{
|
15 |
prompt: string
|
16 |
font: FontName
|
|
|
73 |
// setPage: (page: HTMLDivElement) => void
|
74 |
|
75 |
generate: (prompt: string, presetName: PresetName, layoutName: LayoutName) => void
|
|
|
76 |
convertComicToClap: () => Promise<ClapProject>
|
77 |
+
convertClapToComic: (clap: ClapProject) => Promise<{
|
78 |
+
currentNbPanels: number
|
79 |
+
prompt: string
|
80 |
+
storyPrompt: string
|
81 |
+
stylePrompt: string
|
82 |
+
panels: string[]
|
83 |
+
renderedScenes: Record<string, RenderedScene>
|
84 |
+
captions: string[]
|
85 |
+
}>
|
86 |
+
loadClap: (blob: Blob) => Promise<void>
|
87 |
downloadClap: () => Promise<void>
|
88 |
}>((set, get) => ({
|
89 |
prompt:
|
|
|
416 |
layouts,
|
417 |
})
|
418 |
},
|
419 |
+
|
420 |
convertComicToClap: async (): Promise<ClapProject> => {
|
421 |
const {
|
422 |
currentNbPanels,
|
|
|
508 |
return clap
|
509 |
},
|
510 |
|
511 |
+
convertClapToComic: async (clap: ClapProject): Promise<{
|
512 |
+
currentNbPanels: number
|
513 |
+
prompt: string
|
514 |
+
storyPrompt: string
|
515 |
+
stylePrompt: string
|
516 |
+
panels: string[]
|
517 |
+
renderedScenes: Record<string, RenderedScene>
|
518 |
+
captions: string[]
|
519 |
+
}> => {
|
520 |
+
|
521 |
+
const prompt = clap.meta.description
|
522 |
+
const [stylePrompt, storyPrompt] = prompt.split("||").map(x => x.trim())
|
523 |
+
|
524 |
+
const panels: string[] = []
|
525 |
+
const renderedScenes: Record<string, RenderedScene> = {}
|
526 |
+
const captions: string[] = []
|
527 |
+
|
528 |
+
const panelGenerationStatus: Record<number, boolean> = {}
|
529 |
+
|
530 |
+
const cameraShots = clap.segments.filter(s => s.category === "camera")
|
531 |
+
|
532 |
+
const shots = cameraShots.map(cameraShot => ({
|
533 |
+
camera: cameraShot,
|
534 |
+
storyboard: filterSegments(
|
535 |
+
ClapSegmentFilteringMode.START,
|
536 |
+
cameraShot,
|
537 |
+
clap.segments,
|
538 |
+
"storyboard"
|
539 |
+
).at(0) as (ClapSegment | undefined),
|
540 |
+
ui: filterSegments(
|
541 |
+
ClapSegmentFilteringMode.START,
|
542 |
+
cameraShot,
|
543 |
+
clap.segments,
|
544 |
+
"interface"
|
545 |
+
).at(0) as (ClapSegment | undefined)
|
546 |
+
})).filter(item => item.storyboard && item.ui) as {
|
547 |
+
camera: ClapSegment
|
548 |
+
storyboard: ClapSegment
|
549 |
+
ui: ClapSegment
|
550 |
+
}[]
|
551 |
+
|
552 |
+
shots.forEach(({ camera, storyboard, ui }, id) => {
|
553 |
+
|
554 |
+
panels.push(storyboard.prompt)
|
555 |
+
|
556 |
+
const renderedScene: RenderedScene = {
|
557 |
+
renderId: storyboard.id,
|
558 |
+
status: "pending",
|
559 |
+
assetUrl: "",
|
560 |
+
alt: storyboard.prompt,
|
561 |
+
error: "",
|
562 |
+
maskUrl: "",
|
563 |
+
segments: []
|
564 |
+
}
|
565 |
+
|
566 |
+
if (storyboard.assetUrl) {
|
567 |
+
renderedScene.assetUrl = storyboard.assetUrl
|
568 |
+
renderedScene.status = "pregenerated" // <- special trick to indicate that it should not be re-generated
|
569 |
+
}
|
570 |
+
|
571 |
+
renderedScenes[id] = renderedScene
|
572 |
+
|
573 |
+
panelGenerationStatus[id] = false
|
574 |
+
|
575 |
+
captions.push(ui.prompt)
|
576 |
+
})
|
577 |
+
|
578 |
+
return {
|
579 |
+
currentNbPanels: shots.length,
|
580 |
+
prompt,
|
581 |
+
storyPrompt,
|
582 |
+
stylePrompt,
|
583 |
+
panels,
|
584 |
+
renderedScenes,
|
585 |
+
captions,
|
586 |
+
|
587 |
+
}
|
588 |
+
},
|
589 |
+
|
590 |
+
loadClap: async (blob: Blob) => {
|
591 |
+
const { convertClapToComic, currentNbPanelsPerPage } = get()
|
592 |
+
|
593 |
+
const currentClap = await parseClap(blob)
|
594 |
+
|
595 |
+
const {
|
596 |
+
currentNbPanels,
|
597 |
+
prompt,
|
598 |
+
storyPrompt,
|
599 |
+
stylePrompt,
|
600 |
+
panels,
|
601 |
+
renderedScenes,
|
602 |
+
captions,
|
603 |
+
} = await convertClapToComic(currentClap)
|
604 |
+
|
605 |
+
// kids, don't do this in your projects: use state managers instead!
|
606 |
+
putTextInInput(document.getElementById("top-menu-input-style-prompt") as HTMLInputElement, stylePrompt)
|
607 |
+
putTextInInput(document.getElementById("top-menu-input-story-prompt") as HTMLInputElement, storyPrompt)
|
608 |
+
|
609 |
+
set({
|
610 |
+
currentClap,
|
611 |
+
currentNbPanels,
|
612 |
+
prompt,
|
613 |
+
panels,
|
614 |
+
renderedScenes,
|
615 |
+
captions,
|
616 |
+
currentNbPages: Math.round(currentNbPanels / currentNbPanelsPerPage),
|
617 |
+
upscaleQueue: {},
|
618 |
+
isGeneratingStory: false,
|
619 |
+
isGeneratingText: false,
|
620 |
+
})
|
621 |
+
},
|
622 |
+
|
623 |
downloadClap: async () => {
|
624 |
+
const { convertComicToClap, prompt } = get()
|
625 |
|
626 |
const currentClap = await convertComicToClap()
|
627 |
|
|
|
636 |
const anchor = document.createElement("a")
|
637 |
anchor.href = objectUrl
|
638 |
|
639 |
+
const [stylePrompt, storyPrompt] = prompt.split("||").map(x => x.trim())
|
640 |
+
|
641 |
+
const cleanStylePrompt = stylePrompt.replace(/([a-z0-9_,]+)/gi, "_")
|
642 |
+
const cleanStoryPrompt = storyPrompt.replace(/([a-z0-9_,]+)/gi, "_")
|
643 |
+
const cleanName = `${cleanStoryPrompt.slice(0, 20)} (${cleanStylePrompt.slice(0, 20) || "default style"})`
|
644 |
+
|
645 |
+
anchor.download = `${cleanName}.clap`
|
646 |
|
647 |
document.body.appendChild(anchor) // Append to the body (could be removed once clicked)
|
648 |
anchor.click() // Trigger the download
|
|
|
650 |
// Cleanup: revoke the object URL and remove the anchor element
|
651 |
URL.revokeObjectURL(objectUrl)
|
652 |
document.body.removeChild(anchor)
|
653 |
+
},
|
654 |
}))
|
src/lib/fileToBase64.ts
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export function fileToBase64(file: File | Blob): Promise<string> {
|
2 |
+
return new Promise((resolve, reject) => {
|
3 |
+
const fileReader = new FileReader();
|
4 |
+
fileReader.readAsDataURL(file);
|
5 |
+
fileReader.onload = () => { resolve(`${fileReader.result}`); };
|
6 |
+
fileReader.onerror = (error) => { reject(error); };
|
7 |
+
});
|
8 |
+
}
|
src/lib/putTextInInput.ts
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export function putTextInInput(input?: HTMLInputElement, text: string = "") {
|
2 |
+
if (!input) { return }
|
3 |
+
|
4 |
+
const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(
|
5 |
+
window.HTMLInputElement.prototype,
|
6 |
+
"value"
|
7 |
+
)?.set;
|
8 |
+
|
9 |
+
// fallback
|
10 |
+
if (!nativeTextAreaValueSetter) {
|
11 |
+
input.value = text
|
12 |
+
return
|
13 |
+
}
|
14 |
+
|
15 |
+
nativeTextAreaValueSetter.call(input, text)
|
16 |
+
const event = new Event('input', { bubbles: true });
|
17 |
+
input.dispatchEvent(event)
|
18 |
+
}
|
src/types.ts
CHANGED
@@ -61,6 +61,7 @@ export interface ImageSegment {
|
|
61 |
}
|
62 |
|
63 |
export type RenderedSceneStatus =
|
|
|
64 |
| "pending"
|
65 |
| "completed"
|
66 |
| "error"
|
|
|
61 |
}
|
62 |
|
63 |
export type RenderedSceneStatus =
|
64 |
+
| "pregenerated"
|
65 |
| "pending"
|
66 |
| "completed"
|
67 |
| "error"
|