Spaces:
Sleeping
Sleeping
Julian Bilcke
commited on
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"
|