pls turn it back on

#705
This view is limited to 50 files because it contains too many changes.Β  See the raw diff here.
Files changed (50) hide show
  1. .env +2 -4
  2. .nvmrc +1 -1
  3. README.md +3 -3
  4. package-lock.json +0 -0
  5. package.json +14 -22
  6. src/app/engine/render.ts +11 -15
  7. src/app/interface/about/index.tsx +5 -4
  8. src/app/interface/{advert β†’ ai-clip-factory}/index.tsx +5 -5
  9. src/app/interface/auth-wall/index.tsx +5 -14
  10. src/app/interface/bottom-bar/bottom-bar.tsx +18 -61
  11. src/app/interface/discord/index.tsx +0 -20
  12. src/app/interface/grid/index.tsx +1 -1
  13. src/app/interface/login/login.tsx +1 -1
  14. src/app/interface/page/index.tsx +3 -5
  15. src/app/interface/panel/bubble/index.tsx +2 -3
  16. src/app/interface/panel/index.tsx +20 -67
  17. src/app/interface/select-global-layout/index.tsx +0 -39
  18. src/app/interface/select-layout/index.tsx +0 -56
  19. src/app/interface/settings-dialog/defaultSettings.ts +2 -5
  20. src/app/interface/settings-dialog/getSettings.ts +1 -4
  21. src/app/interface/settings-dialog/index.tsx +36 -174
  22. src/app/interface/settings-dialog/label.tsx +2 -10
  23. src/app/interface/settings-dialog/localStorageKeys.ts +18 -25
  24. src/app/interface/settings-dialog/section-title.tsx +0 -20
  25. src/app/interface/share/index.tsx +5 -5
  26. src/app/interface/top-menu/index.tsx +66 -53
  27. src/app/layouts/index.tsx +0 -15
  28. src/app/layouts/settings.tsx +0 -52
  29. src/app/main.tsx +6 -29
  30. src/app/page.tsx +6 -16
  31. src/app/queries/getDynamicConfig.ts +0 -3
  32. src/app/queries/getLLMEngineFunction.ts +0 -19
  33. src/app/queries/getStoryContinuation.ts +1 -6
  34. src/app/queries/getSystemPrompt.ts +3 -3
  35. src/app/queries/mockLLMResponse.ts +3 -11
  36. src/app/queries/predict.ts +13 -21
  37. src/app/queries/predictNextPanels.ts +12 -25
  38. src/app/queries/predictWithAnthropic.ts +8 -15
  39. src/app/queries/predictWithGroq.ts +7 -15
  40. src/app/queries/predictWithHuggingFace.ts +6 -4
  41. src/app/queries/predictWithOpenAI.ts +8 -18
  42. src/app/store/index.ts +16 -348
  43. src/lib/bubble/injectSpeechBubbleInTheBackground.ts +0 -543
  44. src/lib/createLlamaPrompt.ts +1 -1
  45. src/lib/dirtyGeneratedPanelCleaner.ts +0 -3
  46. src/lib/dirtyGeneratedPanelsParser.ts +2 -5
  47. src/lib/fileToBase64.ts +0 -8
  48. src/lib/getImageDimension.ts +2 -12
  49. src/lib/getLocalStorageShowSpeeches.ts +0 -13
  50. src/lib/getOAuthRedirectUrl.ts +0 -11
.env CHANGED
@@ -24,8 +24,6 @@ NEXT_PUBLIC_ENABLE_RATE_LIMITER="false"
24
  ENABLE_HUGGING_FACE_OAUTH=
25
  ENABLE_HUGGING_FACE_OAUTH_WALL=
26
  HUGGING_FACE_OAUTH_CLIENT_ID=
27
-
28
- # in production this should be the space's domain and/or URL
29
  HUGGING_FACE_OAUTH_REDIRECT_URL=
30
 
31
  # this one must be kept secret (and is unused for now)
@@ -72,7 +70,7 @@ RENDERING_HF_INFERENCE_API_FILE_TYPE="image/png"
72
 
73
  # An experimental RENDERING engine (sorry it is not very documented yet, so you can use one of the other engines)
74
  RENDERING_VIDEOCHAIN_API_URL="http://localhost:7860"
75
-
76
  RENDERING_OPENAI_API_BASE_URL="https://api.openai.com/v1"
77
  RENDERING_OPENAI_API_MODEL="dall-e-3"
78
 
@@ -82,7 +80,7 @@ LLM_GROQ_API_MODEL="mixtral-8x7b-32768"
82
 
83
  # If you decide to use OpenAI for the LLM engine
84
  LLM_OPENAI_API_BASE_URL="https://api.openai.com/v1"
85
- LLM_OPENAI_API_MODEL="gpt-4-turbo"
86
 
87
  # If you decide to use Anthropic (eg. Claude) for the LLM engine
88
  # https://docs.anthropic.com/claude/docs/models-overview
 
24
  ENABLE_HUGGING_FACE_OAUTH=
25
  ENABLE_HUGGING_FACE_OAUTH_WALL=
26
  HUGGING_FACE_OAUTH_CLIENT_ID=
 
 
27
  HUGGING_FACE_OAUTH_REDIRECT_URL=
28
 
29
  # this one must be kept secret (and is unused for now)
 
70
 
71
  # An experimental RENDERING engine (sorry it is not very documented yet, so you can use one of the other engines)
72
  RENDERING_VIDEOCHAIN_API_URL="http://localhost:7860"
73
+ you decide
74
  RENDERING_OPENAI_API_BASE_URL="https://api.openai.com/v1"
75
  RENDERING_OPENAI_API_MODEL="dall-e-3"
76
 
 
80
 
81
  # If you decide to use OpenAI for the LLM engine
82
  LLM_OPENAI_API_BASE_URL="https://api.openai.com/v1"
83
+ LLM_OPENAI_API_MODEL="gpt-4"
84
 
85
  # If you decide to use Anthropic (eg. Claude) for the LLM engine
86
  # https://docs.anthropic.com/claude/docs/models-overview
.nvmrc CHANGED
@@ -1 +1 @@
1
- v20.17.0
 
1
+ v20.9.0
README.md CHANGED
@@ -6,7 +6,7 @@ colorTo: yellow
6
  sdk: docker
7
  pinned: true
8
  app_port: 3000
9
- disable_embedding: false
10
  short_description: Create your own AI comic with a single prompt
11
  hf_oauth: true
12
  hf_oauth_expiration_minutes: 43200
@@ -55,7 +55,7 @@ Language model config (depending on the LLM engine you decide to use):
55
  - `LLM_HF_INFERENCE_ENDPOINT_URL`: "<use your own>"
56
  - `LLM_HF_INFERENCE_API_MODEL`: "HuggingFaceH4/zephyr-7b-beta"
57
  - `LLM_OPENAI_API_BASE_URL`: "https://api.openai.com/v1"
58
- - `LLM_OPENAI_API_MODEL`: "gpt-4-turbo"
59
  - `LLM_GROQ_API_MODEL`: "mixtral-8x7b-32768"
60
  - `LLM_ANTHROPIC_API_MODEL`: "claude-3-opus-20240229"
61
 
@@ -123,7 +123,7 @@ LLM_ENGINE="OPENAI"
123
  # default openai api base url is: https://api.openai.com/v1
124
  LLM_OPENAI_API_BASE_URL="A custom OpenAI API Base URL if you have some special privileges"
125
 
126
- LLM_OPENAI_API_MODEL="gpt-4-turbo"
127
 
128
  AUTH_OPENAI_API_KEY="Yourown OpenAI API Key"
129
  ```
 
6
  sdk: docker
7
  pinned: true
8
  app_port: 3000
9
+ disable_embedding: true
10
  short_description: Create your own AI comic with a single prompt
11
  hf_oauth: true
12
  hf_oauth_expiration_minutes: 43200
 
55
  - `LLM_HF_INFERENCE_ENDPOINT_URL`: "<use your own>"
56
  - `LLM_HF_INFERENCE_API_MODEL`: "HuggingFaceH4/zephyr-7b-beta"
57
  - `LLM_OPENAI_API_BASE_URL`: "https://api.openai.com/v1"
58
+ - `LLM_OPENAI_API_MODEL`: "gpt-4"
59
  - `LLM_GROQ_API_MODEL`: "mixtral-8x7b-32768"
60
  - `LLM_ANTHROPIC_API_MODEL`: "claude-3-opus-20240229"
61
 
 
123
  # default openai api base url is: https://api.openai.com/v1
124
  LLM_OPENAI_API_BASE_URL="A custom OpenAI API Base URL if you have some special privileges"
125
 
126
+ LLM_OPENAI_API_MODEL="gpt-3.5-turbo"
127
 
128
  AUTH_OPENAI_API_KEY="Yourown OpenAI API Key"
129
  ```
package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "@jbilcke/comic-factory",
3
- "version": "1.2.3",
4
  "private": true,
5
  "scripts": {
6
  "dev": "next dev",
@@ -9,11 +9,9 @@
9
  "lint": "next lint"
10
  },
11
  "dependencies": {
12
- "@aitube/clap": "0.2.4",
13
- "@anthropic-ai/sdk": "^0.25.0",
14
- "@huggingface/hub": "^0.15.1",
15
- "@huggingface/inference": "^2.0.0",
16
- "@mediapipe/tasks-vision": "0.10.15",
17
  "@radix-ui/react-accordion": "^1.1.2",
18
  "@radix-ui/react-avatar": "^1.0.3",
19
  "@radix-ui/react-checkbox": "^1.0.4",
@@ -32,8 +30,8 @@
32
  "@radix-ui/react-toast": "^1.1.4",
33
  "@radix-ui/react-tooltip": "^1.0.6",
34
  "@types/node": "20.4.2",
35
- "@types/react": "18.3.0",
36
- "@types/react-dom": "18.3.0",
37
  "@types/uuid": "^9.0.2",
38
  "autoprefixer": "10.4.18",
39
  "class-variance-authority": "^0.6.1",
@@ -46,37 +44,31 @@
46
  "eslint-config-next": "13.4.10",
47
  "groq-sdk": "^0.3.1",
48
  "html2canvas": "^1.4.1",
49
- "i": "^0.3.7",
50
  "konva": "^9.2.2",
51
  "lucide-react": "^0.260.0",
52
- "next": "14.2.7",
53
- "npm": "^10.7.0",
54
  "openai": "^4.29.2",
55
  "pick": "^0.0.1",
56
  "postcss": "8.4.37",
57
- "query-string": "^9.0.0",
58
- "react": "18.3.1",
59
  "react-circular-progressbar": "^2.1.0",
60
  "react-contenteditable": "^3.3.7",
61
- "react-dom": "18.3.1",
62
  "react-draggable": "^4.4.6",
63
- "react-hook-consent": "^3.5.3",
64
  "react-icons": "^4.11.0",
65
  "react-konva": "^18.2.10",
66
  "react-virtualized-auto-sizer": "^1.0.20",
67
- "replicate": "^0.32.0",
68
  "sbd": "^1.0.19",
69
- "sharp": "^0.33.4",
70
  "tailwind-merge": "^2.2.2",
71
  "tailwindcss": "3.4.1",
72
  "tailwindcss-animate": "^1.0.6",
73
  "ts-node": "^10.9.1",
74
- "typescript": "^5.4.5",
75
- "use-file-picker": "^2.1.2",
76
- "usehooks-ts": "2.9.1",
77
  "uuid": "^9.0.0",
78
- "yaml": "^2.4.5",
79
- "zustand": "^4.5.1"
80
  },
81
  "devDependencies": {
82
  "@types/qs": "^6.9.7",
 
1
  {
2
  "name": "@jbilcke/comic-factory",
3
+ "version": "1.2.0",
4
  "private": true,
5
  "scripts": {
6
  "dev": "next dev",
 
9
  "lint": "next lint"
10
  },
11
  "dependencies": {
12
+ "@anthropic-ai/sdk": "^0.19.1",
13
+ "@huggingface/hub": "^0.14.2",
14
+ "@huggingface/inference": "^2.6.1",
 
 
15
  "@radix-ui/react-accordion": "^1.1.2",
16
  "@radix-ui/react-avatar": "^1.0.3",
17
  "@radix-ui/react-checkbox": "^1.0.4",
 
30
  "@radix-ui/react-toast": "^1.1.4",
31
  "@radix-ui/react-tooltip": "^1.0.6",
32
  "@types/node": "20.4.2",
33
+ "@types/react": "18.2.15",
34
+ "@types/react-dom": "18.2.7",
35
  "@types/uuid": "^9.0.2",
36
  "autoprefixer": "10.4.18",
37
  "class-variance-authority": "^0.6.1",
 
44
  "eslint-config-next": "13.4.10",
45
  "groq-sdk": "^0.3.1",
46
  "html2canvas": "^1.4.1",
 
47
  "konva": "^9.2.2",
48
  "lucide-react": "^0.260.0",
49
+ "next": "14.1.4",
 
50
  "openai": "^4.29.2",
51
  "pick": "^0.0.1",
52
  "postcss": "8.4.37",
53
+ "react": "18.2.0",
 
54
  "react-circular-progressbar": "^2.1.0",
55
  "react-contenteditable": "^3.3.7",
56
+ "react-dom": "18.2.0",
57
  "react-draggable": "^4.4.6",
 
58
  "react-icons": "^4.11.0",
59
  "react-konva": "^18.2.10",
60
  "react-virtualized-auto-sizer": "^1.0.20",
61
+ "replicate": "^0.29.0",
62
  "sbd": "^1.0.19",
63
+ "sharp": "^0.33.2",
64
  "tailwind-merge": "^2.2.2",
65
  "tailwindcss": "3.4.1",
66
  "tailwindcss-animate": "^1.0.6",
67
  "ts-node": "^10.9.1",
68
+ "typescript": "5.1.6",
69
+ "usehooks-ts": "^2.9.1",
 
70
  "uuid": "^9.0.0",
71
+ "zustand": "^4.4.1"
 
72
  },
73
  "devDependencies": {
74
  "@types/qs": "^6.9.7",
src/app/engine/render.ts CHANGED
@@ -84,8 +84,6 @@ export async function newRender({
84
 
85
  const placeholder = "<USE YOUR OWN TOKEN>"
86
 
87
- const negativePrompt = "speech bubble, caption, subtitle"
88
-
89
  // console.log("settings:", JSON.stringify(settings, null, 2))
90
 
91
  if (
@@ -189,21 +187,20 @@ export async function newRender({
189
  segments: []
190
  } as RenderedScene
191
  } else if (renderingEngine === "REPLICATE") {
192
- if (!replicateApiKey || `${replicateApiKey || ""}`.length < 8) {
193
  throw new Error(`invalid replicateApiKey, you need to configure your REPLICATE_API_TOKEN in order to use the REPLICATE rendering engine`)
194
  }
195
-
196
  if (!replicateApiModel) {
197
  throw new Error(`invalid replicateApiModel, you need to configure your REPLICATE_API_MODEL in order to use the REPLICATE rendering engine`)
198
  }
199
-
 
 
200
  const replicate = new Replicate({ auth: replicateApiKey })
201
 
202
  const seed = generateSeed()
203
  const prediction = await replicate.predictions.create({
204
- model: replicateApiModelVersion
205
- ? `${replicateApiModel}:${replicateApiModelVersion}`
206
- : `${replicateApiModel}`,
207
  input: {
208
  prompt: [
209
  "beautiful",
@@ -224,7 +221,7 @@ export async function newRender({
224
 
225
  // no need to reply straight away as images take time to generate, this isn't instantaneous
226
  // also our friends at Replicate won't like it if we spam them with requests
227
- await sleep(1000)
228
 
229
  return {
230
  renderId: prediction.id,
@@ -245,6 +242,9 @@ export async function newRender({
245
  if (renderingEngine === "INFERENCE_API" && !huggingfaceInferenceApiModel) {
246
  throw new Error(`invalid huggingfaceInferenceApiModel, you need to configure your RENDERING_HF_INFERENCE_API_BASE_MODEL in order to use the INFERENCE_API rendering engine`)
247
  }
 
 
 
248
 
249
  const baseModelUrl = renderingEngine === "INFERENCE_ENDPOINT"
250
  ? huggingfaceApiUrl
@@ -301,7 +301,7 @@ export async function newRender({
301
  // note: there is no "refiner" step yet for custom inference endpoint
302
  // you probably don't need it anyway, as you probably want to deploy an all-in-one model instead for perf reasons
303
 
304
- if (renderingEngine === "INFERENCE_API" && huggingfaceInferenceApiModelRefinerModel) {
305
  try {
306
  const refinerModelUrl = `https://api-inference.huggingface.co/models/${huggingfaceInferenceApiModelRefinerModel}`
307
 
@@ -315,7 +315,6 @@ export async function newRender({
315
  inputs: Buffer.from(blob).toString('base64'),
316
  parameters: {
317
  prompt: positivePrompt,
318
- negative_prompt: negativePrompt,
319
  num_inference_steps: nbInferenceSteps,
320
  guidance_scale: guidanceScale,
321
  width,
@@ -370,10 +369,7 @@ export async function newRender({
370
  },
371
  body: JSON.stringify({
372
  prompt,
373
- negativePrompt,
374
-
375
- // for a future version of the comic factory
376
- identityImage: "",
377
 
378
  nbFrames,
379
 
 
84
 
85
  const placeholder = "<USE YOUR OWN TOKEN>"
86
 
 
 
87
  // console.log("settings:", JSON.stringify(settings, null, 2))
88
 
89
  if (
 
187
  segments: []
188
  } as RenderedScene
189
  } else if (renderingEngine === "REPLICATE") {
190
+ if (!replicateApiKey) {
191
  throw new Error(`invalid replicateApiKey, you need to configure your REPLICATE_API_TOKEN in order to use the REPLICATE rendering engine`)
192
  }
 
193
  if (!replicateApiModel) {
194
  throw new Error(`invalid replicateApiModel, you need to configure your REPLICATE_API_MODEL in order to use the REPLICATE rendering engine`)
195
  }
196
+ if (!replicateApiModelVersion) {
197
+ throw new Error(`invalid replicateApiModelVersion, you need to configure your REPLICATE_API_MODEL_VERSION in order to use the REPLICATE rendering engine`)
198
+ }
199
  const replicate = new Replicate({ auth: replicateApiKey })
200
 
201
  const seed = generateSeed()
202
  const prediction = await replicate.predictions.create({
203
+ version: replicateApiModelVersion,
 
 
204
  input: {
205
  prompt: [
206
  "beautiful",
 
221
 
222
  // no need to reply straight away as images take time to generate, this isn't instantaneous
223
  // also our friends at Replicate won't like it if we spam them with requests
224
+ await sleep(4000)
225
 
226
  return {
227
  renderId: prediction.id,
 
242
  if (renderingEngine === "INFERENCE_API" && !huggingfaceInferenceApiModel) {
243
  throw new Error(`invalid huggingfaceInferenceApiModel, you need to configure your RENDERING_HF_INFERENCE_API_BASE_MODEL in order to use the INFERENCE_API rendering engine`)
244
  }
245
+ if (renderingEngine === "INFERENCE_API" && !huggingfaceInferenceApiModelRefinerModel) {
246
+ throw new Error(`invalid huggingfaceInferenceApiModelRefinerModel, you need to configure your RENDERING_HF_INFERENCE_API_REFINER_MODEL in order to use the INFERENCE_API rendering engine`)
247
+ }
248
 
249
  const baseModelUrl = renderingEngine === "INFERENCE_ENDPOINT"
250
  ? huggingfaceApiUrl
 
301
  // note: there is no "refiner" step yet for custom inference endpoint
302
  // you probably don't need it anyway, as you probably want to deploy an all-in-one model instead for perf reasons
303
 
304
+ if (renderingEngine === "INFERENCE_API") {
305
  try {
306
  const refinerModelUrl = `https://api-inference.huggingface.co/models/${huggingfaceInferenceApiModelRefinerModel}`
307
 
 
315
  inputs: Buffer.from(blob).toString('base64'),
316
  parameters: {
317
  prompt: positivePrompt,
 
318
  num_inference_steps: nbInferenceSteps,
319
  guidance_scale: guidanceScale,
320
  width,
 
369
  },
370
  body: JSON.stringify({
371
  prompt,
372
+ // negativePrompt, unused for now
 
 
 
373
 
374
  nbFrames,
375
 
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.6`
12
- const APP_RELEASE_DATE = `August 2024`
13
 
14
  const ExternalLink = ({ url, children }: { url: string; children: ReactNode }) => {
15
  return (
@@ -27,12 +27,13 @@ export function About() {
27
  <Dialog open={isOpen} onOpenChange={setOpen}>
28
  <DialogTrigger asChild>
29
  <Button variant="outline">
30
- <span className="hidden md:inline">About</span>
31
- <span className="inline md:hidden">?</span>
32
  </Button>
33
  </DialogTrigger>
34
  <DialogContent className="w-full sm:max-w-[500px] md:max-w-[600px] overflow-y-scroll h-[100vh] sm:h-[550px]">
35
  <DialogHeader>
 
36
  <DialogDescription className="w-full text-center text-2xl font-bold text-stone-700">
37
  <ExternalLink url={APP_URL}>{APP_DOMAIN}</ExternalLink> {APP_VERSION} ({APP_RELEASE_DATE})
38
  </DialogDescription>
 
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.2`
12
+ const APP_RELEASE_DATE = `March 2024`
13
 
14
  const ExternalLink = ({ url, children }: { url: string; children: ReactNode }) => {
15
  return (
 
27
  <Dialog open={isOpen} onOpenChange={setOpen}>
28
  <DialogTrigger asChild>
29
  <Button variant="outline">
30
+ <span className="hidden md:inline">{APP_NAME.replaceAll(" ", "-")} {APP_VERSION}</span>
31
+ <span className="inline md:hidden">Version {APP_VERSION}</span>
32
  </Button>
33
  </DialogTrigger>
34
  <DialogContent className="w-full sm:max-w-[500px] md:max-w-[600px] overflow-y-scroll h-[100vh] sm:h-[550px]">
35
  <DialogHeader>
36
+ <DialogTitle><ExternalLink url={APP_URL}>{APP_DOMAIN}</ExternalLink> {APP_VERSION}</DialogTitle>
37
  <DialogDescription className="w-full text-center text-2xl font-bold text-stone-700">
38
  <ExternalLink url={APP_URL}>{APP_DOMAIN}</ExternalLink> {APP_VERSION} ({APP_RELEASE_DATE})
39
  </DialogDescription>
src/app/interface/{advert β†’ ai-clip-factory}/index.tsx RENAMED
@@ -1,15 +1,15 @@
1
  import { Button } from "@/components/ui/button"
2
 
3
- export function Advert() {
4
  return (
5
  <Button
6
  variant="outline"
7
- className="bg-yellow-400 border-stone-600/30 hover:bg-yellow-300"
8
  onClick={() => {
9
- window.open("https://huggingface.co/spaces/jbilcke-hf/ai-stories-factory", "_blank")
10
  }}>
11
- <span className="hidden md:inline">Make AI stories</span>
12
- <span className="inline md:hidden">...</span>
13
  </Button>
14
  )
15
  }
 
1
  import { Button } from "@/components/ui/button"
2
 
3
+ export function AIClipFactory() {
4
  return (
5
  <Button
6
  variant="outline"
7
+ className="bg-yellow-300"
8
  onClick={() => {
9
+ window.open("https://huggingface.co/spaces/jbilcke-hf/ai-clip-factory?postId=f63df23d-de2f-4dee-961c-a56f160dd159&prompt=pikachu%2C+working+on+a+computer%2C+office%2C+serious%2C+typing%2C+keyboard&model=TheLastBen%2FPikachu_SDXL", "_blank")
10
  }}>
11
+ <span className="hidden md:inline">Try the clip factory!</span>
12
+ <span className="inline md:hidden">Clips</span>
13
  </Button>
14
  )
15
  }
src/app/interface/auth-wall/index.tsx CHANGED
@@ -2,31 +2,22 @@
2
  import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
3
 
4
  import { Login } from "../login"
5
- import { SettingsDialog } from "../settings-dialog"
6
 
7
  export function AuthWall({ show }: { show: boolean }) {
8
  return (
9
  <Dialog open={show}>
10
- <DialogContent className="sm:max-w-[800px]">
11
- <div className="grid gap-4 py-4 text-stone-800 text-center text-xl">
12
  <p className="">
13
- The AI Comic Factory is a free app compatible with many vendors.
14
  </p>
15
  <p>
16
- By default it uses Hugging Face for story and image generation,<br/>
17
- our service is free of charge but we would like you to sign-in πŸ‘‡
18
  </p>
19
  <p>
20
  <Login />
21
  </p>
22
- {/*<p>(if login doesn&apos;t work for you, please use the button in the About panel)</p>*/}
23
- <p className="mt-2 text-lg">
24
- To hide this message, you can also go in the <SettingsDialog /> to replace<br/>
25
- both the image and the story providers to use external vendors.
26
- </p>
27
- <p className="mt-2 text-base">
28
- This pop-up will also disappear if you <a className="text-stone-600 underline" href="https://github.com/jbilcke-hf/ai-comic-factory" target="_blank">download the code</a> to run the app at home.
29
- </p>
30
  </div>
31
  </DialogContent>
32
  </Dialog>
 
2
  import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
3
 
4
  import { Login } from "../login"
 
5
 
6
  export function AuthWall({ show }: { show: boolean }) {
7
  return (
8
  <Dialog open={show}>
9
+ <DialogContent className="sm:max-w-[425px]">
10
+ <div className="grid gap-4 py-4 text-stone-800">
11
  <p className="">
12
+ The AI Comic Factory is a free app available to all Hugging Face users!
13
  </p>
14
  <p>
15
+ Please sign-in to continue:
 
16
  </p>
17
  <p>
18
  <Login />
19
  </p>
20
+ <p>(temporary issue alert: if this doesn&apos;t work for you, please use the button in the About panel)</p>
 
 
 
 
 
 
 
21
  </div>
22
  </DialogContent>
23
  </Dialog>
src/app/interface/bottom-bar/bottom-bar.tsx CHANGED
@@ -1,5 +1,4 @@
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"
@@ -9,43 +8,32 @@ import { sleep } from "@/lib/sleep"
9
 
10
  import { Share } from "../share"
11
  import { About } from "../about"
12
- import { Discord } from "../discord"
13
  import { SettingsDialog } from "../settings-dialog"
14
  import { useLocalStorage } from "usehooks-ts"
15
  import { localStorageKeys } from "../settings-dialog/localStorageKeys"
16
  import { defaultSettings } from "../settings-dialog/defaultSettings"
17
- import { getParam } from "@/lib/getParam"
18
- import { Advert } from "../advert"
19
-
20
 
21
  function BottomBar() {
22
  // deprecated, as HTML-to-bitmap didn't work that well for us
23
- // const page = useStore(s => s.page)
24
- // const download = useStore(s => s.download)
25
- // const pageToImage = useStore(s => s.pageToImage)
26
 
27
- const isGeneratingStory = useStore(s => s.isGeneratingStory)
28
- const prompt = useStore(s => s.prompt)
29
- const panelGenerationStatus = useStore(s => s.panelGenerationStatus)
30
 
31
- const preset = useStore(s => s.preset)
32
-
33
- const canSeeBetaFeatures = false // getParam<boolean>("beta", false)
34
 
35
  const allStatus = Object.values(panelGenerationStatus)
36
  const remainingImages = allStatus.reduce((acc, s) => (acc + (s ? 1 : 0)), 0)
37
 
38
- const currentClap = useStore(s => s.currentClap)
39
-
40
- const upscaleQueue = useStore(s => s.upscaleQueue)
41
- const renderedScenes = useStore(s => s.renderedScenes)
42
- const removeFromUpscaleQueue = useStore(s => s.removeFromUpscaleQueue)
43
- const setRendered = useStore(s => s.setRendered)
44
  const [isUpscaling, setUpscaling] = useState(false)
45
 
46
- const loadClap = useStore(s => s.loadClap)
47
- const downloadClap = useStore(s => s.downloadClap)
48
-
49
  const [hasGeneratedAtLeastOnce, setHasGeneratedAtLeastOnce] = useLocalStorage<boolean>(
50
  localStorageKeys.hasGeneratedAtLeastOnce,
51
  defaultSettings.hasGeneratedAtLeastOnce
@@ -93,27 +81,6 @@ function BottomBar() {
93
  }
94
  }, [hasFinishedGeneratingImages, hasGeneratedAtLeastOnce])
95
 
96
- const { openFilePicker, filesContent } = useFilePicker({
97
- accept: '.clap',
98
- readAs: "ArrayBuffer"
99
- })
100
- const fileData = filesContent[0]
101
-
102
- useEffect(() => {
103
- const fn = async () => {
104
- if (fileData?.name) {
105
- try {
106
- const blob = new Blob([fileData.content])
107
- await loadClap(blob)
108
- } catch (err) {
109
- console.error("failed to load the Clap file:", err)
110
- }
111
- }
112
- }
113
- fn()
114
- }, [fileData?.name])
115
-
116
-
117
  return (
118
  <div className={cn(
119
  `print:hidden`,
@@ -132,8 +99,10 @@ function BottomBar() {
132
  `scale-[0.9]`
133
  )}>
134
  <About />
135
- <Discord />
136
- <Advert />
 
 
137
  </div>
138
  <div className={cn(
139
  `flex flex-row`,
@@ -176,30 +145,18 @@ function BottomBar() {
176
  </Button>
177
  </div>
178
  */}
179
- {canSeeBetaFeatures ? <Button
180
- onClick={openFilePicker}
181
- disabled={remainingImages > 0}
182
- >Load</Button> : null}
183
- {canSeeBetaFeatures ? <Button
184
- onClick={downloadClap}
185
- disabled={remainingImages > 0}
186
- >
187
- {remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} βŒ›` : `Save`}
188
- </Button> : null}
189
-
190
  <Button
191
  onClick={handlePrint}
192
  disabled={!prompt?.length}
193
  >
194
  <span className="hidden md:inline">{
195
- remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} panels βŒ›` : `Get PDF`
196
  }</span>
197
  <span className="inline md:hidden">{
198
- remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} βŒ›` : `PDF`
199
  }</span>
200
  </Button>
201
-
202
- <Share />
203
  </div>
204
  </div>
205
  )
 
1
  import { startTransition, useEffect, useState } from "react"
 
2
 
3
  import { useStore } from "@/app/store"
4
  import { Button } from "@/components/ui/button"
 
8
 
9
  import { Share } from "../share"
10
  import { About } from "../about"
 
11
  import { SettingsDialog } from "../settings-dialog"
12
  import { useLocalStorage } from "usehooks-ts"
13
  import { localStorageKeys } from "../settings-dialog/localStorageKeys"
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)
30
 
31
+ const upscaleQueue = useStore(state => state.upscaleQueue)
32
+ const renderedScenes = useStore(state => state.renderedScenes)
33
+ const removeFromUpscaleQueue = useStore(state => state.removeFromUpscaleQueue)
34
+ const setRendered = useStore(state => state.setRendered)
 
 
35
  const [isUpscaling, setUpscaling] = useState(false)
36
 
 
 
 
37
  const [hasGeneratedAtLeastOnce, setHasGeneratedAtLeastOnce] = useLocalStorage<boolean>(
38
  localStorageKeys.hasGeneratedAtLeastOnce,
39
  defaultSettings.hasGeneratedAtLeastOnce
 
81
  }
82
  }, [hasFinishedGeneratingImages, hasGeneratedAtLeastOnce])
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  return (
85
  <div className={cn(
86
  `print:hidden`,
 
99
  `scale-[0.9]`
100
  )}>
101
  <About />
102
+ {/*
103
+ Thank you clip factory for your service 🫑
104
+ <AIClipFactory />
105
+ */}
106
  </div>
107
  <div className={cn(
108
  `flex flex-row`,
 
145
  </Button>
146
  </div>
147
  */}
 
 
 
 
 
 
 
 
 
 
 
148
  <Button
149
  onClick={handlePrint}
150
  disabled={!prompt?.length}
151
  >
152
  <span className="hidden md:inline">{
153
+ remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} panels βŒ›` : `Save PDF`
154
  }</span>
155
  <span className="inline md:hidden">{
156
+ remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} βŒ›` : `Save`
157
  }</span>
158
  </Button>
159
+ <Share />
 
160
  </div>
161
  </div>
162
  )
src/app/interface/discord/index.tsx DELETED
@@ -1,20 +0,0 @@
1
- import { FaDiscord } from "react-icons/fa"
2
-
3
-
4
- export function Discord() {
5
- return (
6
- <a
7
- className="
8
- flex flex-row items-center justify-center
9
- h-10
10
- no-underline
11
- animation-all duration-150 ease-in-out
12
- text-stone-700 hover:text-stone-950 scale-95 hover:scale-100"
13
- href="https://discord.gg/AEruz9B92B"
14
- target="_blank">
15
- <div><FaDiscord size={24} /></div>
16
- <span className="text-sm ml-1.5 hidden md:inline">Discord</span>
17
- <span className="text-sm ml-1.5 inline md:hidden"></span>
18
- </a>
19
- )
20
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/interface/grid/index.tsx CHANGED
@@ -6,7 +6,7 @@ import { cn } from "@/lib/utils"
6
  import { useStore } from "@/app/store"
7
 
8
  export function Grid({ children, className }: { children: ReactNode; className: string }) {
9
- const zoomLevel = useStore(s => s.zoomLevel)
10
 
11
  return (
12
  <div
 
6
  import { useStore } from "@/app/store"
7
 
8
  export function Grid({ children, className }: { children: ReactNode; className: string }) {
9
+ const zoomLevel = useStore(state => state.zoomLevel)
10
 
11
  return (
12
  <div
src/app/interface/login/login.tsx CHANGED
@@ -7,7 +7,7 @@ import { useOAuth } from "@/lib/useOAuth"
7
 
8
  function Login() {
9
  const { login } = useOAuth({ debug: false })
10
- return <Button onClick={login} className="text-xl">Sign-in with Hugging Face</Button>
11
  }
12
 
13
  export default Login
 
7
 
8
  function Login() {
9
  const { login } = useOAuth({ debug: false })
10
+ return <Button onClick={login}>Sign-in with Hugging Face</Button>
11
  }
12
 
13
  export default Login
src/app/interface/page/index.tsx CHANGED
@@ -7,8 +7,8 @@ import { useStore } from "@/app/store"
7
  import { cn } from "@/lib/utils"
8
 
9
  export function Page({ page }: { page: number }) {
10
- const zoomLevel = useStore(s => s.zoomLevel)
11
- const layouts = useStore(s => s.layouts)
12
 
13
  // attention: here we use a fallback to layouts[0]
14
  // if no predetermined layout exists for this page number
@@ -39,11 +39,9 @@ export function Page({ page }: { page: number }) {
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
- // update: in the future if we want a good html to image convertion
44
  /*
45
 
46
- const setPage = useStore(s => s.setPage)
47
  const pageRef = useRef<HTMLDivElement>(null)
48
 
49
  useEffect(() => {
 
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
 
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)
46
 
47
  useEffect(() => {
src/app/interface/panel/bubble/index.tsx CHANGED
@@ -14,9 +14,8 @@ export function Bubble({ children, onChange }: {
14
  }) {
15
 
16
  const ref = useRef<HTMLDivElement>(null)
17
- const zoomLevel = useStore(s => s.zoomLevel)
18
- const showSpeeches = useStore(s => s.showSpeeches)
19
- const showCaptions = useStore(s => s.showCaptions)
20
 
21
  const text = useRef(`${children || ''}`)
22
 
 
14
  }) {
15
 
16
  const ref = useRef<HTMLDivElement>(null)
17
+ const zoomLevel = useStore(state => state.zoomLevel)
18
+ const showCaptions = useStore(state => state.showCaptions)
 
19
 
20
  const text = useRef(`${children || ''}`)
21
 
src/app/interface/panel/index.tsx CHANGED
@@ -2,23 +2,22 @@
2
 
3
  import { useEffect, useRef, useState, useTransition } from "react"
4
  import { RxReload, RxPencil2 } from "react-icons/rx"
5
- import { useLocalStorage } from "usehooks-ts"
6
 
7
  import { RenderedScene, RenderingModelVendor } from "@/types"
 
8
  import { getRender, newRender } from "@/app/engine/render"
9
  import { useStore } from "@/app/store"
10
- import { injectSpeechBubbleInTheBackground } from "@/lib/bubble/injectSpeechBubbleInTheBackground"
11
  import { cn } from "@/lib/utils"
12
  import { getInitialRenderedScene } from "@/lib/getInitialRenderedScene"
13
  import { Progress } from "@/app/interface/progress"
14
-
15
  import { EditModal } from "../edit-modal"
 
16
  import { getSettings } from "../settings-dialog/getSettings"
 
17
  import { localStorageKeys } from "../settings-dialog/localStorageKeys"
18
  import { defaultSettings } from "../settings-dialog/defaultSettings"
19
 
20
- import { Bubble } from "./bubble"
21
-
22
  export function Panel({
23
  page,
24
  nbPanels,
@@ -36,47 +35,45 @@ export function Panel({
36
  // panel id, between 0 and (nbPanels - 1)
37
  panel: number
38
 
 
39
  className?: string
40
  width?: number
41
  height?: number
42
  }) {
 
43
  // index of the panel in the whole app
44
  const panelIndex = page * nbPanels + panel
45
 
 
46
  // the panel Id must be unique across all pages
47
  const panelId = `${panelIndex}`
48
 
49
  // console.log(`panel/index.tsx: <Panel panelId=${panelId}> rendered again!`)
50
 
 
51
  const [mouseOver, setMouseOver] = useState(false)
52
  const ref = useRef<HTMLImageElement>(null)
53
- const font = useStore(s => s.font)
54
- const preset = useStore(s => s.preset)
55
 
56
- const setGeneratingImages = useStore(s => s.setGeneratingImages)
57
 
58
- const panels = useStore(s => s.panels)
59
  const prompt = panels[panelIndex] || ""
60
 
61
- const setPanelPrompt = useStore(s => s.setPanelPrompt)
62
-
63
- const showSpeeches = useStore(s => s.showSpeeches)
64
 
65
- const speeches = useStore(s => s.speeches)
66
- const speech = speeches[panelIndex] || ""
67
- const setPanelSpeech = useStore(s => s.setPanelSpeech)
68
-
69
- const captions = useStore(s => s.captions)
70
  const caption = captions[panelIndex] || ""
71
- const setPanelCaption = useStore(s => s.setPanelCaption)
72
 
73
- const zoomLevel = useStore(s => s.zoomLevel)
74
 
75
- const addToUpscaleQueue = useStore(s => s.addToUpscaleQueue)
76
 
77
  const [_isPending, startTransition] = useTransition()
78
- const renderedScenes = useStore(s => s.renderedScenes)
79
- const setRendered = useStore(s => s.setRendered)
80
 
81
  const rendered = renderedScenes[panelIndex] || getInitialRenderedScene()
82
 
@@ -98,31 +95,6 @@ export function Panel({
98
 
99
  let delay = enableRateLimiter ? (1000 + (500 * panelIndex)) : 1000
100
 
101
-
102
- const addSpeechBubble = async () => {
103
- if (!renderedRef.current) { return }
104
-
105
- // story generation failed
106
- if (speech.trim() === "...") { return }
107
-
108
- if (!showSpeeches) { return }
109
-
110
- console.log('Generating speech bubbles (this is experimental!)')
111
- try {
112
- const result = await injectSpeechBubbleInTheBackground({
113
- inputImageInBase64: renderedRef.current.assetUrl,
114
- text: speech,
115
- shape: "oval",
116
- line: "straight", // "straight", "bubble", "chaotic"
117
- // font?: string;
118
- // debug: true,
119
- })
120
- renderedRef.current.assetUrl = result
121
- setRendered(panelId, renderedRef.current)
122
- } catch (err) {
123
- console.log(`error: failed to inject the speech bubble: ${err}`)
124
- }
125
- }
126
  /*
127
  console.log("panel/index.tsx: DEBUG: " + JSON.stringify({
128
  page,
@@ -232,7 +204,6 @@ export function Panel({
232
  if (newRendered.status === "completed") {
233
  setGeneratingImages(panelId, false)
234
  addToUpscaleQueue(panelId, newRendered)
235
- addSpeechBubble()
236
  } else if (!newRendered.status || newRendered.status === "error") {
237
  setGeneratingImages(panelId, false)
238
  } else {
@@ -303,7 +274,6 @@ export function Panel({
303
  console.log("panel finished!")
304
  setGeneratingImages(panelId, false)
305
  addToUpscaleQueue(panelId, newRendered)
306
- addSpeechBubble()
307
 
308
  }
309
  } catch (err) {
@@ -316,17 +286,6 @@ export function Panel({
316
  useEffect(() => {
317
  if (!prompt.length) { return }
318
 
319
- const renderedScene: RenderedScene | undefined = useStore.getState().renderedScenes[panelIndex]
320
-
321
- // console.log("renderedScene:", renderedScene)
322
-
323
- // I'm trying to find a rule to handle the case were we load a .clap file
324
- // I think we should trash all the Panel objects for this to work properly
325
- if (renderedScene && renderedScene.status === "pregenerated" && renderedScene.assetUrl) {
326
- console.log(`loading a pre-generated panel..`)
327
- return
328
- }
329
-
330
  startImageGeneration({ prompt, width, height, nbFrames, revision })
331
 
332
  clearTimeout(timeoutRef.current)
@@ -497,13 +456,7 @@ export function Panel({
497
  height={height}
498
  alt={rendered.alt}
499
  className={cn(
500
- `comic-panel w-full h-full`,
501
- `object-cover`,
502
-
503
- // I think we can remove this to improve compatibility,
504
- // in case the generate image isn't exactly the same size
505
- // `max-w-max`,
506
-
507
  // showCaptions ? `-mt-11` : ''
508
  )}
509
  />}
 
2
 
3
  import { useEffect, useRef, useState, useTransition } from "react"
4
  import { RxReload, RxPencil2 } from "react-icons/rx"
 
5
 
6
  import { RenderedScene, RenderingModelVendor } from "@/types"
7
+
8
  import { getRender, newRender } from "@/app/engine/render"
9
  import { useStore } from "@/app/store"
10
+
11
  import { cn } from "@/lib/utils"
12
  import { getInitialRenderedScene } from "@/lib/getInitialRenderedScene"
13
  import { Progress } from "@/app/interface/progress"
 
14
  import { EditModal } from "../edit-modal"
15
+ import { Bubble } from "./bubble"
16
  import { getSettings } from "../settings-dialog/getSettings"
17
+ import { useLocalStorage } from "usehooks-ts"
18
  import { localStorageKeys } from "../settings-dialog/localStorageKeys"
19
  import { defaultSettings } from "../settings-dialog/defaultSettings"
20
 
 
 
21
  export function Panel({
22
  page,
23
  nbPanels,
 
35
  // panel id, between 0 and (nbPanels - 1)
36
  panel: number
37
 
38
+
39
  className?: string
40
  width?: number
41
  height?: number
42
  }) {
43
+
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)
56
+ const font = useStore(state => state.font)
57
+ const preset = useStore(state => state.preset)
58
 
59
+ const setGeneratingImages = useStore(state => state.setGeneratingImages)
60
 
61
+ const panels = useStore(state => state.panels)
62
  const prompt = panels[panelIndex] || ""
63
 
64
+ const setPanelPrompt = useStore(state => state.setPanelPrompt)
 
 
65
 
66
+ const captions = useStore(state => state.captions)
 
 
 
 
67
  const caption = captions[panelIndex] || ""
68
+ const setPanelCaption = useStore(state => state.setPanelCaption)
69
 
70
+ const zoomLevel = useStore(state => state.zoomLevel)
71
 
72
+ const addToUpscaleQueue = useStore(state => state.addToUpscaleQueue)
73
 
74
  const [_isPending, startTransition] = useTransition()
75
+ const renderedScenes = useStore(state => state.renderedScenes)
76
+ const setRendered = useStore(state => state.setRendered)
77
 
78
  const rendered = renderedScenes[panelIndex] || getInitialRenderedScene()
79
 
 
95
 
96
  let delay = enableRateLimiter ? (1000 + (500 * panelIndex)) : 1000
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  /*
99
  console.log("panel/index.tsx: DEBUG: " + JSON.stringify({
100
  page,
 
204
  if (newRendered.status === "completed") {
205
  setGeneratingImages(panelId, false)
206
  addToUpscaleQueue(panelId, newRendered)
 
207
  } else if (!newRendered.status || newRendered.status === "error") {
208
  setGeneratingImages(panelId, false)
209
  } else {
 
274
  console.log("panel finished!")
275
  setGeneratingImages(panelId, false)
276
  addToUpscaleQueue(panelId, newRendered)
 
277
 
278
  }
279
  } catch (err) {
 
286
  useEffect(() => {
287
  if (!prompt.length) { return }
288
 
 
 
 
 
 
 
 
 
 
 
 
289
  startImageGeneration({ prompt, width, height, nbFrames, revision })
290
 
291
  clearTimeout(timeoutRef.current)
 
456
  height={height}
457
  alt={rendered.alt}
458
  className={cn(
459
+ `comic-panel w-full h-full object-cover max-w-max`,
 
 
 
 
 
 
460
  // showCaptions ? `-mt-11` : ''
461
  )}
462
  />}
src/app/interface/select-global-layout/index.tsx DELETED
@@ -1,39 +0,0 @@
1
- "use client"
2
-
3
- import { useEffect, useState } from "react"
4
- import { useSearchParams } from "next/navigation"
5
-
6
- import { useStore } from "@/app/store"
7
- import { LayoutName, defaultLayout, nonRandomLayouts } from "@/app/layouts"
8
- import { useIsBusy } from "@/lib/useIsBusy"
9
-
10
- import { SelectLayout } from "../select-layout"
11
-
12
- export function SelectGlobalLayout() {
13
- const searchParams = useSearchParams()
14
-
15
- const requestedLayout = (searchParams?.get('layout') as LayoutName) || defaultLayout
16
-
17
- const layout = useStore(s => s.layout)
18
- const setLayout = useStore(s => s.setLayout)
19
-
20
- const isBusy = useIsBusy()
21
-
22
- const [draftLayout, setDraftLayout] = useState<LayoutName>(requestedLayout)
23
-
24
- useEffect(() => {
25
- const layoutChanged = draftLayout !== layout
26
- if (layoutChanged && !isBusy) {
27
- setLayout(draftLayout)
28
- }
29
- }, [layout, draftLayout, isBusy])
30
-
31
- return (
32
- <SelectLayout
33
- defaultValue={defaultLayout}
34
- onLayoutChange={setDraftLayout}
35
- disabled={isBusy}
36
- layouts={nonRandomLayouts}
37
- />
38
- )
39
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/interface/select-layout/index.tsx DELETED
@@ -1,56 +0,0 @@
1
- "use client"
2
-
3
- import Image from "next/image"
4
-
5
- import {
6
- Select,
7
- SelectContent,
8
- SelectItem,
9
- SelectTrigger,
10
- SelectValue,
11
- } from "@/components/ui/select"
12
- import { LayoutName, allLayoutLabels, defaultLayout, layoutIcons } from "@/app/layouts"
13
-
14
- export function SelectLayout({
15
- defaultValue = defaultLayout,
16
- onLayoutChange,
17
- disabled = false,
18
- layouts = [],
19
- }: {
20
- defaultValue?: string | undefined
21
- onLayoutChange?: ((name: LayoutName) => void)
22
- disabled?: boolean
23
- layouts: string[]
24
- }) {
25
- return (
26
- <Select
27
- defaultValue={defaultValue}
28
- onValueChange={(name) => { onLayoutChange?.(name as LayoutName) }}
29
- disabled={disabled}
30
- >
31
- <SelectTrigger className="flex-grow bg-gray-100 text-gray-700 dark:bg-gray-100 dark:text-gray-700">
32
- <SelectValue className="text-2xs md:text-sm" placeholder="Layout" />
33
- </SelectTrigger>
34
- <SelectContent>
35
- {layouts.map(key =>
36
- <SelectItem key={key} value={key} className="w-full">
37
- <div className="space-x-6 flex flex-row items-center justify-between">
38
- <div className="flex">{
39
- (allLayoutLabels as any)[key]
40
- }</div>
41
-
42
- {(layoutIcons as any)[key]
43
- ? <Image
44
- className="rounded-sm opacity-75"
45
- src={(layoutIcons as any)[key]}
46
- width={20}
47
- height={18}
48
- alt={key}
49
- /> : null}
50
- </div>
51
- </SelectItem>
52
- )}
53
- </SelectContent>
54
- </Select>
55
- )
56
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/interface/settings-dialog/defaultSettings.ts CHANGED
@@ -1,9 +1,8 @@
1
- import { LLMVendor, RenderingModelVendor, Settings } from "@/types"
2
 
3
  export const defaultSettings: Settings = {
4
  renderingModelVendor: "SERVER" as RenderingModelVendor,
5
  renderingUseTurbo: false,
6
- llmVendor: "SERVER" as LLMVendor,
7
  huggingFaceOAuth: "",
8
  huggingfaceApiKey: "",
9
  huggingfaceInferenceApiModel: "stabilityai/stable-diffusion-xl-base-1.0",
@@ -15,11 +14,9 @@ export const defaultSettings: Settings = {
15
  replicateApiModelTrigger: "",
16
  openaiApiKey: "",
17
  openaiApiModel: "dall-e-3",
18
- openaiApiLanguageModel: "gpt-4-turbo",
19
  groqApiKey: "",
20
  groqApiLanguageModel: "mixtral-8x7b-32768",
21
- anthropicApiKey: "",
22
- anthropicApiLanguageModel: "claude-3-opus-20240229",
23
  hasGeneratedAtLeastOnce: false,
24
  userDefinedMaxNumberOfPages: 1,
25
  }
 
1
+ import { RenderingModelVendor, Settings } from "@/types"
2
 
3
  export const defaultSettings: Settings = {
4
  renderingModelVendor: "SERVER" as RenderingModelVendor,
5
  renderingUseTurbo: false,
 
6
  huggingFaceOAuth: "",
7
  huggingfaceApiKey: "",
8
  huggingfaceInferenceApiModel: "stabilityai/stable-diffusion-xl-base-1.0",
 
14
  replicateApiModelTrigger: "",
15
  openaiApiKey: "",
16
  openaiApiModel: "dall-e-3",
17
+ openaiApiLanguageModel: "gpt-4",
18
  groqApiKey: "",
19
  groqApiLanguageModel: "mixtral-8x7b-32768",
 
 
20
  hasGeneratedAtLeastOnce: false,
21
  userDefinedMaxNumberOfPages: 1,
22
  }
src/app/interface/settings-dialog/getSettings.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { LLMVendor, RenderingModelVendor, Settings } from "@/types"
2
 
3
  import { getValidString } from "@/lib/getValidString"
4
  import { localStorageKeys } from "./localStorageKeys"
@@ -11,7 +11,6 @@ export function getSettings(): Settings {
11
  return {
12
  renderingModelVendor: getValidString(localStorage?.getItem?.(localStorageKeys.renderingModelVendor), defaultSettings.renderingModelVendor) as RenderingModelVendor,
13
  renderingUseTurbo: getValidBoolean(localStorage?.getItem?.(localStorageKeys.renderingUseTurbo), defaultSettings.renderingUseTurbo),
14
- llmVendor: getValidString(localStorage?.getItem?.(localStorageKeys.llmVendor), defaultSettings.llmVendor) as LLMVendor,
15
  huggingFaceOAuth: getValidString(localStorage?.getItem?.(localStorageKeys.huggingFaceOAuth), defaultSettings.huggingFaceOAuth),
16
  huggingfaceApiKey: getValidString(localStorage?.getItem?.(localStorageKeys.huggingfaceApiKey), defaultSettings.huggingfaceApiKey),
17
  huggingfaceInferenceApiModel: getValidString(localStorage?.getItem?.(localStorageKeys.huggingfaceInferenceApiModel), defaultSettings.huggingfaceInferenceApiModel),
@@ -26,8 +25,6 @@ export function getSettings(): Settings {
26
  openaiApiLanguageModel: getValidString(localStorage?.getItem?.(localStorageKeys.openaiApiLanguageModel), defaultSettings.openaiApiLanguageModel),
27
  groqApiKey: getValidString(localStorage?.getItem?.(localStorageKeys.groqApiKey), defaultSettings.groqApiKey),
28
  groqApiLanguageModel: getValidString(localStorage?.getItem?.(localStorageKeys.groqApiLanguageModel), defaultSettings.groqApiLanguageModel),
29
- anthropicApiKey: getValidString(localStorage?.getItem?.(localStorageKeys.anthropicApiKey), defaultSettings.anthropicApiKey),
30
- anthropicApiLanguageModel: getValidString(localStorage?.getItem?.(localStorageKeys.anthropicApiLanguageModel), defaultSettings.anthropicApiLanguageModel),
31
  hasGeneratedAtLeastOnce: getValidBoolean(localStorage?.getItem?.(localStorageKeys.hasGeneratedAtLeastOnce), defaultSettings.hasGeneratedAtLeastOnce),
32
  userDefinedMaxNumberOfPages: getValidNumber(localStorage?.getItem?.(localStorageKeys.userDefinedMaxNumberOfPages), 1, Number.MAX_SAFE_INTEGER, defaultSettings.userDefinedMaxNumberOfPages),
33
  }
 
1
+ import { RenderingModelVendor, Settings } from "@/types"
2
 
3
  import { getValidString } from "@/lib/getValidString"
4
  import { localStorageKeys } from "./localStorageKeys"
 
11
  return {
12
  renderingModelVendor: getValidString(localStorage?.getItem?.(localStorageKeys.renderingModelVendor), defaultSettings.renderingModelVendor) as RenderingModelVendor,
13
  renderingUseTurbo: getValidBoolean(localStorage?.getItem?.(localStorageKeys.renderingUseTurbo), defaultSettings.renderingUseTurbo),
 
14
  huggingFaceOAuth: getValidString(localStorage?.getItem?.(localStorageKeys.huggingFaceOAuth), defaultSettings.huggingFaceOAuth),
15
  huggingfaceApiKey: getValidString(localStorage?.getItem?.(localStorageKeys.huggingfaceApiKey), defaultSettings.huggingfaceApiKey),
16
  huggingfaceInferenceApiModel: getValidString(localStorage?.getItem?.(localStorageKeys.huggingfaceInferenceApiModel), defaultSettings.huggingfaceInferenceApiModel),
 
25
  openaiApiLanguageModel: getValidString(localStorage?.getItem?.(localStorageKeys.openaiApiLanguageModel), defaultSettings.openaiApiLanguageModel),
26
  groqApiKey: getValidString(localStorage?.getItem?.(localStorageKeys.groqApiKey), defaultSettings.groqApiKey),
27
  groqApiLanguageModel: getValidString(localStorage?.getItem?.(localStorageKeys.groqApiLanguageModel), defaultSettings.groqApiLanguageModel),
 
 
28
  hasGeneratedAtLeastOnce: getValidBoolean(localStorage?.getItem?.(localStorageKeys.hasGeneratedAtLeastOnce), defaultSettings.hasGeneratedAtLeastOnce),
29
  userDefinedMaxNumberOfPages: getValidNumber(localStorage?.getItem?.(localStorageKeys.userDefinedMaxNumberOfPages), 1, Number.MAX_SAFE_INTEGER, defaultSettings.userDefinedMaxNumberOfPages),
30
  }
src/app/interface/settings-dialog/index.tsx CHANGED
@@ -13,7 +13,7 @@ import {
13
  SelectValue,
14
  } from "@/components/ui/select"
15
 
16
- import { LLMVendor, RenderingModelVendor } from "@/types"
17
  import { Input } from "@/components/ui/input"
18
 
19
  import { Label } from "./label"
@@ -24,8 +24,6 @@ import { defaultSettings } from "./defaultSettings"
24
  import { useDynamicConfig } from "@/lib/useDynamicConfig"
25
  import { Slider } from "@/components/ui/slider"
26
  import { fonts } from "@/lib/fonts"
27
- import { cn } from "@/lib/utils"
28
- import { SectionTitle } from "./section-title"
29
 
30
  export function SettingsDialog() {
31
  const [isOpen, setOpen] = useState(false)
@@ -37,10 +35,6 @@ export function SettingsDialog() {
37
  localStorageKeys.renderingUseTurbo,
38
  defaultSettings.renderingUseTurbo
39
  )
40
- const [llmVendor, setLlmModelVendor] = useLocalStorage<LLMVendor>(
41
- localStorageKeys.llmVendor,
42
- defaultSettings.llmVendor
43
- )
44
  const [huggingfaceApiKey, setHuggingfaceApiKey] = useLocalStorage<string>(
45
  localStorageKeys.huggingfaceApiKey,
46
  defaultSettings.huggingfaceApiKey
@@ -81,26 +75,6 @@ export function SettingsDialog() {
81
  localStorageKeys.openaiApiModel,
82
  defaultSettings.openaiApiModel
83
  )
84
- const [openaiApiLanguageModel, setOpenaiApiLanguageModel] = useLocalStorage<string>(
85
- localStorageKeys.openaiApiLanguageModel,
86
- defaultSettings.openaiApiLanguageModel
87
- )
88
- const [groqApiKey, setGroqApiKey] = useLocalStorage<string>(
89
- localStorageKeys.groqApiKey,
90
- defaultSettings.groqApiKey
91
- )
92
- const [groqApiLanguageModel, setGroqApiLanguageModel] = useLocalStorage<string>(
93
- localStorageKeys.groqApiLanguageModel,
94
- defaultSettings.groqApiLanguageModel
95
- )
96
- const [anthropicApiKey, setAnthropicApiKey] = useLocalStorage<string>(
97
- localStorageKeys.anthropicApiKey,
98
- defaultSettings.anthropicApiKey
99
- )
100
- const [anthropicApiLanguageModel, setAnthropicApiLanguageModel] = useLocalStorage<string>(
101
- localStorageKeys.anthropicApiLanguageModel,
102
- defaultSettings.anthropicApiLanguageModel
103
- )
104
  const [userDefinedMaxNumberOfPages, setUserDefinedMaxNumberOfPages] = useLocalStorage<number>(
105
  localStorageKeys.userDefinedMaxNumberOfPages,
106
  defaultSettings.userDefinedMaxNumberOfPages
@@ -113,25 +87,19 @@ export function SettingsDialog() {
113
  <DialogTrigger asChild>
114
  <Button className="space-x-1 md:space-x-2">
115
  <div>
116
- <span className="">Settings</span>
117
  </div>
118
  </Button>
119
  </DialogTrigger>
120
- <DialogContent className="w-full sm:max-w-[500px] md:max-w-[700px] bg-gray-100">
121
  <DialogHeader>
122
- <DialogDescription className="w-full text-center text-2xl font-bold text-stone-800">
123
- AI Comic Factory Settings
124
  </DialogDescription>
125
  </DialogHeader>
126
  <div className="overflow-y-scroll h-[75vh] md:h-[70vh]">
127
- <p className="text-base italic text-zinc-600 w-full text-center">
128
- ℹ️ Some models can take time to cold-start, or be under heavy traffic.<br/>
129
- πŸ‘‰ In case of trouble, try again after 5-10 minutes.<br/>
130
- πŸ”’ Your settings are stored inside your browser, not on our servers.
131
- </p>
132
- <SectionTitle>πŸ‘‡ General options</SectionTitle>
133
  {isConfigReady && <Field>
134
- <Label className="pt-2">Move the slider to set the total expected number of pages: {userDefinedMaxNumberOfPages}</Label>
135
  <Slider
136
  min={1}
137
  max={maxNbPages}
@@ -147,11 +115,31 @@ export function SettingsDialog() {
147
  />
148
  </Field>
149
  }
150
- <div className={cn(
151
- `grid gap-2 pt-3 pb-1`,
152
- `text-stone-800`
153
- )}>
154
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
  {
157
  // renderingModelVendor === "SERVER" && <>
@@ -180,29 +168,6 @@ export function SettingsDialog() {
180
  // </>
181
  }
182
 
183
- <SectionTitle>πŸ‘‡ Panel rendering options</SectionTitle>
184
-
185
- <Field>
186
- <Label className={cn(
187
- )}>Image generation - please choose a stable diffusion provider:</Label>
188
- <Select
189
- onValueChange={(value: string) => {
190
- setRenderingModelVendor(value as RenderingModelVendor)
191
- }}
192
- defaultValue={renderingModelVendor}
193
- value={renderingModelVendor}>
194
- <SelectTrigger className="bg-white">
195
- <SelectValue />
196
- </SelectTrigger>
197
- <SelectContent>
198
- <SelectItem value="SERVER">Default Hugging Face server (free but limited capacity, not always online)</SelectItem>
199
- <SelectItem value="HUGGINGFACE">Custom Inference API model (pro hugging face account recommended)</SelectItem>
200
- <SelectItem value="REPLICATE">Custom Replicate model (will bill your own account)</SelectItem>
201
- <SelectItem value="OPENAI">DALLΒ·E 3 by OpenAI (partial support, will bill your own account)</SelectItem>
202
- </SelectContent>
203
- </Select>
204
- </Field>
205
-
206
  {renderingModelVendor === "HUGGINGFACE" && <>
207
  <Field>
208
  <Label>Hugging Face API Token (<a className="text-stone-600 underline" href="https://huggingface.co/subscribe/pro" target="_blank">PRO account</a> recommended for higher rate limit):</Label>
@@ -282,7 +247,7 @@ export function SettingsDialog() {
282
 
283
  {renderingModelVendor === "REPLICATE" && <>
284
  <Field>
285
- <Label>Replicate API Token:</Label>
286
  <Input
287
  className={fonts.actionman.className}
288
  type="password"
@@ -331,113 +296,10 @@ export function SettingsDialog() {
331
  </Field>
332
  </>}
333
 
334
- <SectionTitle>πŸ‘‡ Story generation options (🚧 experimental feature 🚧)</SectionTitle>
335
-
336
- <p>⚠️ Some vendors might be buggy or require tunning, please report issues to Discord.<br/>
337
- ⚠️ Billing and privacy depend on your preferred vendor, so please exercice caution.</p>
338
- <Field>
339
- <Label className={cn(
340
- "mt-2"
341
- )}>Story generation - please choose a LLM provider:</Label>
342
- <Select
343
- onValueChange={(value: string) => {
344
- setLlmModelVendor(value as LLMVendor)
345
- }}
346
- defaultValue={llmVendor}
347
- value={llmVendor}>
348
- <SelectTrigger className="bg-white">
349
- <SelectValue />
350
- </SelectTrigger>
351
- <SelectContent>
352
- <SelectItem value="SERVER">Default Hugging Face server (free but limited capacity, not always online)</SelectItem>
353
- <SelectItem value="GROQ">Open-source models on Groq (will bill your own account)</SelectItem>
354
- <SelectItem value="ANTHROPIC">Claude by Anthropic (will bill your own account)</SelectItem>
355
- <SelectItem value="OPENAI">ChatGPT by OpenAI (will bill your own account)</SelectItem>
356
- </SelectContent>
357
- </Select>
358
- </Field>
359
-
360
- {llmVendor === "GROQ" && <>
361
- <Field>
362
- <Label>Groq API Token:</Label>
363
- <Input
364
- className={fonts.actionman.className}
365
- type="password"
366
- placeholder="Enter your private api token"
367
- onChange={(x) => {
368
- setGroqApiKey(x.target.value)
369
- }}
370
- value={groqApiKey}
371
- />
372
- </Field>
373
- <Field>
374
- <Label>Open-source Model ID:</Label>
375
- <Input
376
- className={fonts.actionman.className}
377
- placeholder="Name of the LLM"
378
- onChange={(x) => {
379
- setGroqApiLanguageModel(x.target.value)
380
- }}
381
- value={groqApiLanguageModel}
382
- />
383
- </Field>
384
- </>}
385
-
386
-
387
- {llmVendor === "ANTHROPIC" && <>
388
- <Field>
389
- <Label>Anthropic API Token:</Label>
390
- <Input
391
- className={fonts.actionman.className}
392
- type="password"
393
- placeholder="Enter your private api token"
394
- onChange={(x) => {
395
- setAnthropicApiKey(x.target.value)
396
- }}
397
- value={anthropicApiKey}
398
- />
399
- </Field>
400
- <Field>
401
- <Label>Proprietary Model ID:</Label>
402
- <Input
403
- className={fonts.actionman.className}
404
- placeholder="Name of the LLM"
405
- onChange={(x) => {
406
- setAnthropicApiLanguageModel(x.target.value)
407
- }}
408
- value={anthropicApiLanguageModel}
409
- />
410
- </Field>
411
- </>}
412
-
413
-
414
- {llmVendor === "OPENAI" && <>
415
- <Field>
416
- <Label>OpenAI API Token:</Label>
417
- <Input
418
- className={fonts.actionman.className}
419
- type="password"
420
- placeholder="Enter your private api token"
421
- onChange={(x) => {
422
- setOpenaiApiKey(x.target.value)
423
- }}
424
- value={openaiApiKey}
425
- />
426
- </Field>
427
- <Field>
428
- <Label>Proprietary Model ID:</Label>
429
- <Input
430
- className={fonts.actionman.className}
431
- placeholder="Name of the LLM"
432
- onChange={(x) => {
433
- setOpenaiApiLanguageModel(x.target.value)
434
- }}
435
- value={openaiApiLanguageModel}
436
- />
437
- </Field>
438
- </>}
439
-
440
- </div>
441
 
442
  </div>
443
 
 
13
  SelectValue,
14
  } from "@/components/ui/select"
15
 
16
+ import { RenderingModelVendor } from "@/types"
17
  import { Input } from "@/components/ui/input"
18
 
19
  import { Label } from "./label"
 
24
  import { useDynamicConfig } from "@/lib/useDynamicConfig"
25
  import { Slider } from "@/components/ui/slider"
26
  import { fonts } from "@/lib/fonts"
 
 
27
 
28
  export function SettingsDialog() {
29
  const [isOpen, setOpen] = useState(false)
 
35
  localStorageKeys.renderingUseTurbo,
36
  defaultSettings.renderingUseTurbo
37
  )
 
 
 
 
38
  const [huggingfaceApiKey, setHuggingfaceApiKey] = useLocalStorage<string>(
39
  localStorageKeys.huggingfaceApiKey,
40
  defaultSettings.huggingfaceApiKey
 
75
  localStorageKeys.openaiApiModel,
76
  defaultSettings.openaiApiModel
77
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  const [userDefinedMaxNumberOfPages, setUserDefinedMaxNumberOfPages] = useLocalStorage<number>(
79
  localStorageKeys.userDefinedMaxNumberOfPages,
80
  defaultSettings.userDefinedMaxNumberOfPages
 
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}
 
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">
122
+ ℹ️ Some API vendors have a delay for rarely used models.<br/>
123
+ πŸ‘‰ In case of trouble, try again after 5-10 minutes.
124
+ </p>
125
+
126
+ <Select
127
+ onValueChange={(value: string) => {
128
+ setRenderingModelVendor(value as RenderingModelVendor)
129
+ }}
130
+ defaultValue={renderingModelVendor}>
131
+ <SelectTrigger className="">
132
+ <SelectValue placeholder="Theme" />
133
+ </SelectTrigger>
134
+ <SelectContent>
135
+ <SelectItem value="SERVER">Use server settings (default)</SelectItem>
136
+ <SelectItem value="HUGGINGFACE">Custom Hugging Face model (recommended)</SelectItem>
137
+ <SelectItem value="REPLICATE">Custom Replicate model (will use your own account)</SelectItem>
138
+ <SelectItem value="OPENAI">DALLΒ·E 3 by OpenAI (partial support, will use your own account)</SelectItem>
139
+ </SelectContent>
140
+ </Select>
141
+ </Field>
142
+
143
 
144
  {
145
  // renderingModelVendor === "SERVER" && <>
 
168
  // </>
169
  }
170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  {renderingModelVendor === "HUGGINGFACE" && <>
172
  <Field>
173
  <Label>Hugging Face API Token (<a className="text-stone-600 underline" href="https://huggingface.co/subscribe/pro" target="_blank">PRO account</a> recommended for higher rate limit):</Label>
 
247
 
248
  {renderingModelVendor === "REPLICATE" && <>
249
  <Field>
250
+ <Label>Replicate API Token (you will be billed based on Replicate pricing):</Label>
251
  <Input
252
  className={fonts.actionman.className}
253
  type="password"
 
296
  </Field>
297
  </>}
298
 
299
+ <p className="text-sm text-zinc-700 italic">
300
+ πŸ”’ Settings such as API keys are stored inside your browser and aren&apos;t kept on our servers.
301
+ </p>
302
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
  </div>
305
 
src/app/interface/settings-dialog/label.tsx CHANGED
@@ -1,15 +1,7 @@
1
  import { ReactNode } from "react"
2
 
3
- import { cn } from "@/lib/utils"
4
-
5
- export function Label({ className, children }: {
6
- className?: string
7
- children: ReactNode
8
- }) {
9
  return (
10
- <label className={cn(
11
- `text-base font-semibold text-zinc-700`,
12
- className
13
- )}>{children}</label>
14
  )
15
  }
 
1
  import { ReactNode } from "react"
2
 
3
+ export function Label({ children }: { children: ReactNode }) {
 
 
 
 
 
4
  return (
5
+ <label className="text-xl font-semibold text-zinc-700">{children}</label>
 
 
 
6
  )
7
  }
src/app/interface/settings-dialog/localStorageKeys.ts CHANGED
@@ -1,29 +1,22 @@
1
  import { Settings } from "@/types"
2
 
3
- // let's keep it "version 0" for now, so as to not disrupt current users
4
- // however at some point we might need to upgrade and invalid the default values
5
- const version = ``
6
-
7
  export const localStorageKeys: Record<keyof Settings, string> = {
8
- renderingModelVendor: `${version}CONF_RENDERING_MODEL_VENDOR`,
9
- renderingUseTurbo: `${version}CONF_RENDERING_USE_TURBO`,
10
- llmVendor: `${version}CONF_LLM_MODEL_VENDOR`,
11
- huggingFaceOAuth: `${version}CONF_AUTH_HF_OAUTH`,
12
- huggingfaceApiKey: `${version}CONF_AUTH_HF_API_TOKEN`,
13
- huggingfaceInferenceApiModel: `${version}CONF_RENDERING_HF_INFERENCE_API_BASE_MODEL`,
14
- huggingfaceInferenceApiModelTrigger: `${version}CONF_RENDERING_HF_INFERENCE_API_BASE_MODEL_TRIGGER`,
15
- huggingfaceInferenceApiFileType: `${version}CONF_RENDERING_HF_INFERENCE_API_FILE_TYPE`,
16
- replicateApiKey: `${version}CONF_AUTH_REPLICATE_API_TOKEN`,
17
- replicateApiModel: `${version}CONF_RENDERING_REPLICATE_API_MODEL`,
18
- replicateApiModelVersion: `${version}CONF_RENDERING_REPLICATE_API_MODEL_VERSION`,
19
- replicateApiModelTrigger: `${version}CONF_RENDERING_REPLICATE_API_MODEL_TRIGGER`,
20
- openaiApiKey: `${version}CONF_AUTH_OPENAI_API_KEY`,
21
- openaiApiModel: `${version}CONF_AUTH_OPENAI_API_MODEL`,
22
- openaiApiLanguageModel: `${version}CONF_AUTH_OPENAI_API_LANGUAGE_MODEL`,
23
- groqApiKey: `${version}CONF_AUTH_GROQ_API_KEY`,
24
- groqApiLanguageModel: `${version}CONF_AUTH_GROQ_API_LANGUAGE_MODEL`,
25
- anthropicApiKey: `${version}CONF_AUTH_ANTHROPIC_API_KEY`,
26
- anthropicApiLanguageModel: `${version}CONF_AUTH_ANTHROPIC_API_LANGUAGE_MODEL`,
27
- hasGeneratedAtLeastOnce: `${version}CONF_HAS_GENERATED_AT_LEAST_ONCE`,
28
- userDefinedMaxNumberOfPages: `${version}CONF_USER_DEFINED_MAX_NUMBER_OF_PAGES`,
29
  }
 
1
  import { Settings } from "@/types"
2
 
 
 
 
 
3
  export const localStorageKeys: Record<keyof Settings, string> = {
4
+ renderingModelVendor: "CONF_RENDERING_MODEL_VENDOR",
5
+ renderingUseTurbo: "CONF_RENDERING_USE_TURBO",
6
+ huggingFaceOAuth: "CONF_AUTH_HF_OAUTH",
7
+ huggingfaceApiKey: "CONF_AUTH_HF_API_TOKEN",
8
+ huggingfaceInferenceApiModel: "CONF_RENDERING_HF_INFERENCE_API_BASE_MODEL",
9
+ huggingfaceInferenceApiModelTrigger: "CONF_RENDERING_HF_INFERENCE_API_BASE_MODEL_TRIGGER",
10
+ huggingfaceInferenceApiFileType: "CONF_RENDERING_HF_INFERENCE_API_FILE_TYPE",
11
+ replicateApiKey: "CONF_AUTH_REPLICATE_API_TOKEN",
12
+ replicateApiModel: "CONF_RENDERING_REPLICATE_API_MODEL",
13
+ replicateApiModelVersion: "CONF_RENDERING_REPLICATE_API_MODEL_VERSION",
14
+ replicateApiModelTrigger: "CONF_RENDERING_REPLICATE_API_MODEL_TRIGGER",
15
+ openaiApiKey: "CONF_AUTH_OPENAI_API_KEY",
16
+ openaiApiModel: "CONF_AUTH_OPENAI_API_MODEL",
17
+ openaiApiLanguageModel: "CONF_AUTH_OPENAI_API_LANGUAGE_MODEL",
18
+ groqApiKey: "CONF_AUTH_GROQ_API_KEY",
19
+ groqApiLanguageModel: "CONF_AUTH_GROQ_API_LANGUAGE_MODEL",
20
+ hasGeneratedAtLeastOnce: "CONF_HAS_GENERATED_AT_LEAST_ONCE",
21
+ userDefinedMaxNumberOfPages: "CONF_USER_DEFINED_MAX_NUMBER_OF_PAGES"
 
 
 
22
  }
src/app/interface/settings-dialog/section-title.tsx DELETED
@@ -1,20 +0,0 @@
1
- import { ReactNode } from "react"
2
-
3
- import { cn } from "@/lib/utils"
4
-
5
- export function SectionTitle({ className, children }: {
6
- className?: string
7
- children: ReactNode
8
- }) {
9
- return (
10
- <div className={cn(
11
- `flex flex-col items-center justify-center`,
12
- `mt-6 pt-4 pb-1 w-full`,
13
- `border-t border-t-stone-400`,
14
- `text-xl font-semibold text-zinc-900`,
15
- className
16
- )}>
17
- {children}
18
- </div>
19
- )
20
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/interface/share/index.tsx CHANGED
@@ -6,9 +6,9 @@ import { useState } from "react"
6
 
7
  export function Share() {
8
  const [isOpen, setOpen] = useState(false)
9
- const preset = useStore(s => s.preset)
10
- const prompt = useStore(s => s.prompt)
11
- const panelGenerationStatus = useStore(s => s.panelGenerationStatus)
12
  const allStatus = Object.values(panelGenerationStatus)
13
  const remainingImages = allStatus.reduce((acc, s) => (acc + (s ? 1 : 0)), 0)
14
 
@@ -119,10 +119,10 @@ ${comicFileMd}`;
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>
 
6
 
7
  export function Share() {
8
  const [isOpen, setOpen] = useState(false)
9
+ const preset = useStore(state => state.preset)
10
+ const prompt = useStore(state => state.prompt)
11
+ const panelGenerationStatus = useStore(state => state.panelGenerationStatus)
12
  const allStatus = Object.values(panelGenerationStatus)
13
  const remainingImages = allStatus.reduce((acc, s) => (acc + (s ? 1 : 0)), 0)
14
 
 
119
  disabled={!prompt?.length}
120
  >
121
  <span className="hidden md:inline">{
122
+ remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} panels βŒ›` : `Save PDF`
123
  }</span>
124
  <span className="inline md:hidden">{
125
+ remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} βŒ›` : `Save`
126
  }</span>
127
  </Button>
128
  </p>
src/app/interface/top-menu/index.tsx CHANGED
@@ -2,6 +2,7 @@
2
 
3
  import { useEffect, useState } from "react"
4
  import { useSearchParams } from "next/navigation"
 
5
  import { StaticImageData } from "next/image"
6
  import { useLocalStorage } from "usehooks-ts"
7
 
@@ -19,58 +20,64 @@ import { Input } from "@/components/ui/input"
19
  import { PresetName, defaultPreset, nonRandomPresets, presets } from "@/app/engine/presets"
20
  import { useStore } from "@/app/store"
21
  import { Button } from "@/components/ui/button"
22
- import { LayoutName, defaultLayout, nonRandomLayouts } from "@/app/layouts"
23
  import { Switch } from "@/components/ui/switch"
24
  import { useOAuth } from "@/lib/useOAuth"
25
- import { useIsBusy } from "@/lib/useIsBusy"
26
 
 
 
 
 
27
  import { localStorageKeys } from "../settings-dialog/localStorageKeys"
28
  import { defaultSettings } from "../settings-dialog/defaultSettings"
29
  import { AuthWall } from "../auth-wall"
30
- import { SelectLayout } from "../select-layout"
31
- import { getLocalStorageShowSpeeches } from "@/lib/getLocalStorageShowSpeeches"
32
 
33
- export function TopMenu() {
34
- const searchParams = useSearchParams()
 
 
 
 
 
35
 
36
- const requestedPreset = (searchParams?.get('preset') as PresetName) || defaultPreset
37
- const requestedFont = (searchParams?.get('font') as FontName) || defaultFont
38
- const requestedStylePrompt = (searchParams?.get('stylePrompt') as string) || ""
39
- const requestedStoryPrompt = (searchParams?.get('storyPrompt') as string) || ""
40
- const requestedLayout = (searchParams?.get('layout') as LayoutName) || defaultLayout
41
-
42
- // const font = useStore(s => s.font)
43
- // const setFont = useStore(s => s.setFont)
44
- const preset = useStore(s => s.preset)
45
- const prompt = useStore(s => s.prompt)
46
- const layout = useStore(s => s.layout)
47
- const setLayout = useStore(s => s.setLayout)
48
 
49
- const setShowSpeeches = useStore(s => s.setShowSpeeches)
50
- const showSpeeches = useStore(s => s.showSpeeches)
51
 
52
- const setShowCaptions = useStore(s => s.setShowCaptions)
53
- const showCaptions = useStore(s => s.showCaptions)
54
 
55
- const currentNbPages = useStore(s => s.currentNbPages)
56
- const setCurrentNbPages = useStore(s => s.setCurrentNbPages)
57
 
58
- const generate = useStore(s => s.generate)
 
 
59
 
60
- const isBusy = useIsBusy()
61
 
62
  const [lastDraftPromptA, setLastDraftPromptA] = useLocalStorage<string>(
63
  "AI_COMIC_FACTORY_LAST_DRAFT_PROMPT_A",
64
- requestedStylePrompt
65
  )
66
 
67
  const [lastDraftPromptB, setLastDraftPromptB] = useLocalStorage<string>(
68
  "AI_COMIC_FACTORY_LAST_DRAFT_PROMPT_B",
69
- requestedStoryPrompt
70
  )
71
 
 
 
 
 
 
 
72
 
73
- // TODO should be in the store
74
  const [draftPromptA, setDraftPromptA] = useState(lastDraftPromptA)
75
  const [draftPromptB, setDraftPromptB] = useState(lastDraftPromptB)
76
  const draftPrompt = `${draftPromptA}||${draftPromptB}`
@@ -93,11 +100,6 @@ export function TopMenu() {
93
  useEffect(() => { if (lastDraftPromptB !== draftPromptB) { setLastDraftPromptB(draftPromptB) } }, [draftPromptB])
94
  useEffect(() => { if (lastDraftPromptB !== draftPromptB) { setDraftPromptB(lastDraftPromptB) } }, [lastDraftPromptB])
95
 
96
- // we need a use effect to properly read the local storage
97
- useEffect(() => {
98
- setShowSpeeches(getLocalStorageShowSpeeches(true))
99
- }, [])
100
-
101
  const handleSubmit = () => {
102
  if (enableOAuthWall && hasGeneratedAtLeastOnce && !isLoggedIn) {
103
  setShowAuthWall(true)
@@ -163,12 +165,36 @@ export function TopMenu() {
163
 
164
  {/* <Label className="flex text-2xs md:text-sm md:w-24">Style:</Label> */}
165
 
166
- <SelectLayout
167
  defaultValue={defaultLayout}
168
- onLayoutChange={setDraftLayout}
169
  disabled={isBusy}
170
- layouts={nonRandomLayouts}
171
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  </div>
173
  <div className="flex flex-row items-center space-x-3">
174
  <Switch
@@ -176,19 +202,8 @@ export function TopMenu() {
176
  onCheckedChange={setShowCaptions}
177
  />
178
  <Label className="text-gray-200 dark:text-gray-200">
179
- <span className="hidden lg:inline">πŸ“–&nbsp;Captions</span>
180
- <span className="inline lg:hidden">πŸ“–</span>
181
- </Label>
182
- </div>
183
- <div className="flex flex-row items-center space-x-3">
184
- <Switch
185
- checked={showSpeeches}
186
- onCheckedChange={setShowSpeeches}
187
- defaultChecked={showSpeeches}
188
- />
189
- <Label className="text-gray-200 dark:text-gray-200">
190
- <span className="hidden lg:inline">πŸ’¬&nbsp;Bubbles</span>
191
- <span className="inline lg:hidden">πŸ’¬</span>
192
  </Label>
193
  </div>
194
  {/*
@@ -226,7 +241,6 @@ export function TopMenu() {
226
  <div className="flex flex-row flex-grow w-full">
227
  <div className="flex flex-row flex-grow w-full">
228
  <Input
229
- id="top-menu-input-story-prompt"
230
  placeholder="1. Story (eg. detective dog)"
231
  className={cn(
232
  `w-1/2 rounded-r-none`,
@@ -245,7 +259,6 @@ export function TopMenu() {
245
  value={draftPromptB}
246
  />
247
  <Input
248
- id="top-menu-input-style-prompt"
249
  placeholder="2. Style (eg 'rain, shiba')"
250
  className={cn(
251
  `w-1/2`,
 
2
 
3
  import { useEffect, useState } from "react"
4
  import { useSearchParams } from "next/navigation"
5
+ import Image from "next/image"
6
  import { StaticImageData } from "next/image"
7
  import { useLocalStorage } from "usehooks-ts"
8
 
 
20
  import { PresetName, defaultPreset, nonRandomPresets, presets } from "@/app/engine/presets"
21
  import { useStore } from "@/app/store"
22
  import { Button } from "@/components/ui/button"
23
+ import { LayoutName, allLayoutLabels, defaultLayout, nonRandomLayouts } from "@/app/layouts"
24
  import { Switch } from "@/components/ui/switch"
25
  import { useOAuth } from "@/lib/useOAuth"
 
26
 
27
+ import layoutPreview0 from "../../../../public/layouts/layout0.jpg"
28
+ import layoutPreview1 from "../../../../public/layouts/layout1.jpg"
29
+ import layoutPreview2 from "../../../../public/layouts/layout2.jpg"
30
+ import layoutPreview3 from "../../../../public/layouts/layout3.jpg"
31
  import { localStorageKeys } from "../settings-dialog/localStorageKeys"
32
  import { defaultSettings } from "../settings-dialog/defaultSettings"
33
  import { AuthWall } from "../auth-wall"
 
 
34
 
35
+ const layoutIcons: Partial<Record<LayoutName, StaticImageData>> = {
36
+ Layout0: layoutPreview0,
37
+ Layout1: layoutPreview1,
38
+ Layout2: layoutPreview2,
39
+ Layout3: layoutPreview3,
40
+ Layout4: undefined,
41
+ }
42
 
43
+ export function TopMenu() {
44
+ // const font = useStore(state => state.font)
45
+ // const setFont = useStore(state => state.setFont)
46
+ const preset = useStore(state => state.preset)
47
+ const prompt = useStore(state => state.prompt)
48
+ const layout = useStore(state => state.layout)
49
+ const setLayout = useStore(state => state.setLayout)
 
 
 
 
 
50
 
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)
60
+ const atLeastOnePanelIsBusy = useStore(state => state.atLeastOnePanelIsBusy)
61
+ const isBusy = isGeneratingStory || atLeastOnePanelIsBusy
62
 
 
63
 
64
  const [lastDraftPromptA, setLastDraftPromptA] = useLocalStorage<string>(
65
  "AI_COMIC_FACTORY_LAST_DRAFT_PROMPT_A",
66
+ ""
67
  )
68
 
69
  const [lastDraftPromptB, setLastDraftPromptB] = useLocalStorage<string>(
70
  "AI_COMIC_FACTORY_LAST_DRAFT_PROMPT_B",
71
+ ""
72
  )
73
 
74
+ const searchParams = useSearchParams()
75
+
76
+ const requestedPreset = (searchParams?.get('preset') as PresetName) || defaultPreset
77
+ const requestedFont = (searchParams?.get('font') as FontName) || defaultFont
78
+ const requestedPrompt = (searchParams?.get('prompt') as string) || ""
79
+ const requestedLayout = (searchParams?.get('layout') as LayoutName) || defaultLayout
80
 
 
81
  const [draftPromptA, setDraftPromptA] = useState(lastDraftPromptA)
82
  const [draftPromptB, setDraftPromptB] = useState(lastDraftPromptB)
83
  const draftPrompt = `${draftPromptA}||${draftPromptB}`
 
100
  useEffect(() => { if (lastDraftPromptB !== draftPromptB) { setLastDraftPromptB(draftPromptB) } }, [draftPromptB])
101
  useEffect(() => { if (lastDraftPromptB !== draftPromptB) { setDraftPromptB(lastDraftPromptB) } }, [lastDraftPromptB])
102
 
 
 
 
 
 
103
  const handleSubmit = () => {
104
  if (enableOAuthWall && hasGeneratedAtLeastOnce && !isLoggedIn) {
105
  setShowAuthWall(true)
 
165
 
166
  {/* <Label className="flex text-2xs md:text-sm md:w-24">Style:</Label> */}
167
 
168
+ <Select
169
  defaultValue={defaultLayout}
170
+ onValueChange={(value) => { setDraftLayout(value as LayoutName) }}
171
  disabled={isBusy}
172
+ >
173
+ <SelectTrigger className="flex-grow bg-gray-100 text-gray-700 dark:bg-gray-100 dark:text-gray-700">
174
+ <SelectValue className="text-2xs md:text-sm" placeholder="Layout" />
175
+ </SelectTrigger>
176
+ <SelectContent>
177
+ {nonRandomLayouts.map(key =>
178
+ <SelectItem key={key} value={key} className="w-full">
179
+ <div className="space-x-6 flex flex-row items-center justify-between">
180
+ <div className="flex">{
181
+ (allLayoutLabels as any)[key]
182
+ }</div>
183
+
184
+ {(layoutIcons as any)[key]
185
+ ? <Image
186
+ className="rounded-sm opacity-75"
187
+ src={(layoutIcons as any)[key]}
188
+ width={20}
189
+ height={18}
190
+ alt={key}
191
+ /> : null}
192
+
193
+ </div>
194
+ </SelectItem>
195
+ )}
196
+ </SelectContent>
197
+ </Select>
198
  </div>
199
  <div className="flex flex-row items-center space-x-3">
200
  <Switch
 
202
  onCheckedChange={setShowCaptions}
203
  />
204
  <Label className="text-gray-200 dark:text-gray-200">
205
+ <span className="hidden md:inline">Caption</span>
206
+ <span className="inline md:hidden">Cap.</span>
 
 
 
 
 
 
 
 
 
 
 
207
  </Label>
208
  </div>
209
  {/*
 
241
  <div className="flex flex-row flex-grow w-full">
242
  <div className="flex flex-row flex-grow w-full">
243
  <Input
 
244
  placeholder="1. Story (eg. detective dog)"
245
  className={cn(
246
  `w-1/2 rounded-r-none`,
 
259
  value={draftPromptB}
260
  />
261
  <Input
 
262
  placeholder="2. Style (eg 'rain, shiba')"
263
  className={cn(
264
  `w-1/2`,
src/app/layouts/index.tsx CHANGED
@@ -1,17 +1,10 @@
1
  "use client"
2
 
3
- import { StaticImageData } from "next/image"
4
-
5
  import { Panel } from "@/app/interface/panel"
6
  import { pick } from "@/lib/pick"
7
  import { Grid } from "@/app/interface/grid"
8
  import { LayoutProps } from "@/types"
9
 
10
- import layoutPreview0 from "../../../public/layouts/layout0.jpg"
11
- import layoutPreview1 from "../../../public/layouts/layout1.jpg"
12
- import layoutPreview2 from "../../../public/layouts/layout2.jpg"
13
- import layoutPreview3 from "../../../public/layouts/layout3.jpg"
14
-
15
  export function Layout0({ page, nbPanels }: LayoutProps) {
16
  return (
17
  <Grid className="grid-cols-2 grid-rows-2">
@@ -447,11 +440,3 @@ export const getRandomLayoutName = (): LayoutName => {
447
  export function getRandomLayoutNames(): LayoutName[] {
448
  return nonRandomLayouts.sort(() => Math.random() - 0.5) as LayoutName[]
449
  }
450
-
451
- export const layoutIcons: Partial<Record<LayoutName, StaticImageData>> = {
452
- Layout0: layoutPreview0,
453
- Layout1: layoutPreview1,
454
- Layout2: layoutPreview2,
455
- Layout3: layoutPreview3,
456
- Layout4: undefined,
457
- }
 
1
  "use client"
2
 
 
 
3
  import { Panel } from "@/app/interface/panel"
4
  import { pick } from "@/lib/pick"
5
  import { Grid } from "@/app/interface/grid"
6
  import { LayoutProps } from "@/types"
7
 
 
 
 
 
 
8
  export function Layout0({ page, nbPanels }: LayoutProps) {
9
  return (
10
  <Grid className="grid-cols-2 grid-rows-2">
 
440
  export function getRandomLayoutNames(): LayoutName[] {
441
  return nonRandomLayouts.sort(() => Math.random() - 0.5) as LayoutName[]
442
  }
 
 
 
 
 
 
 
 
src/app/layouts/settings.tsx DELETED
@@ -1,52 +0,0 @@
1
- import { ClapImageRatio } from "@aitube/clap"
2
-
3
- import { LayoutName } from "."
4
-
5
- export type LayoutSettings = {
6
- panel: number
7
- orientation: ClapImageRatio
8
- width: number
9
- height: number
10
- }
11
-
12
- export const layouts: Record<LayoutName, LayoutSettings[]> = {
13
- random: [],
14
- Layout0: [
15
- { panel: 0, orientation: ClapImageRatio.SQUARE, width: 1024, height: 1024 },
16
- { panel: 1, orientation: ClapImageRatio.SQUARE, width: 1024, height: 1024 },
17
- { panel: 2, orientation: ClapImageRatio.SQUARE, width: 1024, height: 1024 },
18
- { panel: 3, orientation: ClapImageRatio.SQUARE, width: 1024, height: 1024 },
19
- ],
20
- Layout1: [
21
- { panel: 0, orientation: ClapImageRatio.LANDSCAPE, width: 1024, height: 768 },
22
- { panel: 1, orientation: ClapImageRatio.PORTRAIT, width: 768, height: 1024 },
23
- { panel: 2, orientation: ClapImageRatio.PORTRAIT, width: 768, height: 1024 },
24
- { panel: 3, orientation: ClapImageRatio.LANDSCAPE, width: 1024, height: 768 },
25
- ],
26
- Layout2: [
27
- { panel: 0, orientation: ClapImageRatio.PORTRAIT, width: 768, height: 1024 },
28
- { panel: 1, orientation: ClapImageRatio.PORTRAIT, width: 768, height: 1024 },
29
- { panel: 2, orientation: ClapImageRatio.PORTRAIT, width: 512, height: 1024 },
30
- { panel: 3, orientation: ClapImageRatio.LANDSCAPE, width: 1024, height: 768 },
31
- ],
32
- Layout3: [
33
- { panel: 0, orientation: ClapImageRatio.LANDSCAPE, width: 1024, height: 768 },
34
- { panel: 1, orientation: ClapImageRatio.PORTRAIT, width: 768, height: 1024 },
35
- { panel: 2, orientation: ClapImageRatio.PORTRAIT, width: 768, height: 1024 },
36
- { panel: 3, orientation: ClapImageRatio.LANDSCAPE, width: 1024, height: 768 },
37
- ],
38
- Layout4: [
39
- { panel: 0, orientation: ClapImageRatio.PORTRAIT, width: 512, height: 1024 },
40
- { panel: 1, orientation: ClapImageRatio.LANDSCAPE, width: 1024, height: 768 },
41
- { panel: 2, orientation: ClapImageRatio.PORTRAIT, width: 768, height: 1024 },
42
- { panel: 3, orientation: ClapImageRatio.LANDSCAPE, width: 1024, height: 512 },
43
- ],
44
- }
45
- /*
46
- Layout5: [
47
- { panel: 0, orientation: ClapImageRatio.SQUARE, width: 1024, height: 1024 },
48
- { panel: 1, orientation: ClapImageRatio.SQUARE, width: 1024, height: 1024 },
49
- { panel: 2, orientation: ClapImageRatio.SQUARE, width: 1024, height: 1024 },
50
- { panel: 3, orientation: ClapImageRatio.SQUARE, width: 1024, height: 1024 },
51
- ]
52
- */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/main.tsx CHANGED
@@ -19,12 +19,11 @@ 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 { useLLMVendorConfig } from "@/lib/useLLMVendorConfig"
23
 
24
  export default function Main() {
25
  const [_isPending, startTransition] = useTransition()
26
 
27
- const llmVendorConfig = useLLMVendorConfig()
28
  const { config, isConfigReady } = useDynamicConfig()
29
  const isGeneratingStory = useStore(s => s.isGeneratingStory)
30
  const setGeneratingStory = useStore(s => s.setGeneratingStory)
@@ -49,11 +48,8 @@ export default function Main() {
49
 
50
  // do we need those?
51
  const renderedScenes = useStore(s => s.renderedScenes)
52
-
53
- const speeches = useStore(s => s.speeches)
54
- const setSpeeches = useStore(s => s.setSpeeches)
55
-
56
  const captions = useStore(s => s.captions)
 
57
  const setCaptions = useStore(s => s.setCaptions)
58
 
59
  const zoomLevel = useStore(s => s.zoomLevel)
@@ -66,7 +62,7 @@ export default function Main() {
66
  )
67
 
68
  const numberOfPanels = Object.keys(panels).length
69
- const panelGenerationStatus = useStore(s => s.panelGenerationStatus)
70
  const allStatus = Object.values(panelGenerationStatus)
71
  const numberOfPendingGenerations = allStatus.reduce((acc, s) => (acc + (s ? 1 : 0)), 0)
72
 
@@ -93,7 +89,7 @@ export default function Main() {
93
  showNextPageButton
94
  }, null, 2))
95
  */
96
-
97
  useEffect(() => {
98
  if (maxNbPages !== userDefinedMaxNumberOfPages) {
99
  setMaxNbPages(userDefinedMaxNumberOfPages)
@@ -104,7 +100,6 @@ export default function Main() {
104
  const ref = useRef({
105
  existingPanels: [] as GeneratedPanel[],
106
  newPanelsPrompts: [] as string[],
107
- newSpeeches: [] as string[],
108
  newCaptions: [] as string[],
109
  prompt: "",
110
  preset: "",
@@ -125,16 +120,6 @@ export default function Main() {
125
  // console.log(`main.tsx: asked to re-generate!!`)
126
  if (!prompt) { return }
127
 
128
-
129
- // a quick and dirty hack to skip prompt regeneration,
130
- // unless the prompt has really changed
131
- if (
132
- prompt === useStore.getState().currentClap?.meta.description
133
- ) {
134
- console.log(`loading a pre-generated comic, so skipping prompt regeneration..`)
135
- return
136
- }
137
-
138
  // if the prompt or preset changed, we clear the cache
139
  // this part is important, otherwise when trying to change the prompt
140
  // we wouldn't still have remnants of the previous comic
@@ -146,7 +131,6 @@ export default function Main() {
146
  ref.current = {
147
  existingPanels: [],
148
  newPanelsPrompts: [],
149
- newSpeeches: [],
150
  newCaptions: [],
151
  prompt,
152
  preset: preset?.label || "",
@@ -205,8 +189,6 @@ export default function Main() {
205
  // existing panels are critical here: this is how we can
206
  // continue over an existing story
207
  existingPanels: ref.current.existingPanels,
208
-
209
- llmVendorConfig,
210
  })
211
  // console.log("LLM generated some new panels:", candidatePanels)
212
 
@@ -219,7 +201,6 @@ export default function Main() {
219
  const endAt = currentPanel + nbPanelsToGenerate
220
  for (let p = startAt; p < endAt; p++) {
221
  ref.current.newCaptions.push(ref.current.existingPanels[p]?.caption.trim() || "...")
222
- ref.current.newSpeeches.push(ref.current.existingPanels[p]?.speech.trim() || "...")
223
  const newPanel = joinWords([
224
 
225
  // what we do here is that ideally we give full control to the LLM for prompting,
@@ -237,19 +218,15 @@ export default function Main() {
237
 
238
  // update the frontend
239
  // console.log("updating the frontend..")
240
- setSpeeches(ref.current.newSpeeches)
241
  setCaptions(ref.current.newCaptions)
242
- setPanels(ref.current.newPanelsPrompts)
243
- setGeneratingStory(false)
244
 
245
- // TODO generate the clap here
246
-
247
  } catch (err) {
248
  console.log("main.tsx: LLM generation failed:", err)
249
  setGeneratingStory(false)
250
  break
251
  }
252
-
253
  if (currentPanel > (currentNbPanels / 2)) {
254
  console.log("main.tsx: we are halfway there, hold tight!")
255
  // setWaitABitMore(true)
 
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()
26
 
 
27
  const { config, isConfigReady } = useDynamicConfig()
28
  const isGeneratingStory = useStore(s => s.isGeneratingStory)
29
  const setGeneratingStory = useStore(s => s.setGeneratingStory)
 
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)
 
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
 
 
89
  showNextPageButton
90
  }, null, 2))
91
  */
92
+
93
  useEffect(() => {
94
  if (maxNbPages !== userDefinedMaxNumberOfPages) {
95
  setMaxNbPages(userDefinedMaxNumberOfPages)
 
100
  const ref = useRef({
101
  existingPanels: [] as GeneratedPanel[],
102
  newPanelsPrompts: [] as string[],
 
103
  newCaptions: [] as string[],
104
  prompt: "",
105
  preset: "",
 
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
 
131
  ref.current = {
132
  existingPanels: [],
133
  newPanelsPrompts: [],
 
134
  newCaptions: [],
135
  prompt,
136
  preset: preset?.label || "",
 
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
 
 
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,
 
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)
src/app/page.tsx CHANGED
@@ -1,19 +1,16 @@
1
  "use server"
2
 
3
- import { ComponentProps } from "react"
4
  import Head from "next/head"
5
- import Script from "next/script"
6
 
 
7
  import { TooltipProvider } from "@/components/ui/tooltip"
 
8
  import { cn } from "@/lib/utils"
9
-
10
- import Main from "./main"
11
-
12
  // import { Maintenance } from "./interface/maintenance"
13
 
14
  // https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
15
 
16
- export default async function IndexPage() {
17
  return (
18
  <>
19
  <Head>
@@ -25,29 +22,22 @@ export default async function IndexPage() {
25
  `light fixed inset-0 w-screen h-screen flex flex-col items-center`,
26
  `bg-zinc-50 text-stone-900 overflow-y-scroll`,
27
 
28
- // important: in "print" mode we need to allow going out of the screen
29
  `inset-auto print:h-auto print:w-auto print:overflow-visible print:relative print:flex-none`
30
  )}>
31
  <TooltipProvider delayDuration={100}>
32
 
33
  <Main />
34
-
35
- {/*
36
-
37
- to display a maintenance page, hide <Main /> and uncomment this unstead:
38
-
39
- <Maintenance />
40
-
41
- */}
42
 
43
  </TooltipProvider>
44
-
45
  <Script src="https://www.googletagmanager.com/gtag/js?id=GTM-WH4MGSHS" />
46
  <Script id="google-analytics">
47
  {`
48
  window.dataLayer = window.dataLayer || [];
49
  function gtag(){dataLayer.push(arguments);}
50
  gtag('js', new Date());
 
51
  gtag('config', 'GTM-WH4MGSHS');
52
  `}
53
  </Script>
 
1
  "use server"
2
 
 
3
  import Head from "next/head"
 
4
 
5
+ import Main from "./main"
6
  import { TooltipProvider } from "@/components/ui/tooltip"
7
+ import Script from "next/script"
8
  import { cn } from "@/lib/utils"
 
 
 
9
  // import { Maintenance } from "./interface/maintenance"
10
 
11
  // https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
12
 
13
+ export default async function IndexPage({ params: { ownerId } }: { params: { ownerId: string }}) {
14
  return (
15
  <>
16
  <Head>
 
22
  `light fixed inset-0 w-screen h-screen flex flex-col items-center`,
23
  `bg-zinc-50 text-stone-900 overflow-y-scroll`,
24
 
25
+ // important: in "print" mode we need to allowing going out of the screen
26
  `inset-auto print:h-auto print:w-auto print:overflow-visible print:relative print:flex-none`
27
  )}>
28
  <TooltipProvider delayDuration={100}>
29
 
30
  <Main />
31
+ {/* <Maintenance /> */}
 
 
 
 
 
 
 
32
 
33
  </TooltipProvider>
 
34
  <Script src="https://www.googletagmanager.com/gtag/js?id=GTM-WH4MGSHS" />
35
  <Script id="google-analytics">
36
  {`
37
  window.dataLayer = window.dataLayer || [];
38
  function gtag(){dataLayer.push(arguments);}
39
  gtag('js', new Date());
40
+
41
  gtag('config', 'GTM-WH4MGSHS');
42
  `}
43
  </Script>
src/app/queries/getDynamicConfig.ts CHANGED
@@ -15,10 +15,7 @@ export async function getDynamicConfig(): Promise<DynamicConfig> {
15
  nbPanelsPerPage,
16
  nbTotalPanelsToGenerate,
17
  oauthClientId: getValidString(process.env.HUGGING_FACE_OAUTH_CLIENT_ID, ""),
18
-
19
- // this doesn't work (conceptually)
20
  oauthRedirectUrl: getValidString(process.env.HUGGING_FACE_OAUTH_REDIRECT_URL, ""),
21
-
22
  oauthScopes: "openid profile inference-api",
23
  enableHuggingFaceOAuth: getValidBoolean(process.env.ENABLE_HUGGING_FACE_OAUTH, false),
24
  enableHuggingFaceOAuthWall: getValidBoolean(process.env.ENABLE_HUGGING_FACE_OAUTH_WALL, false),
 
15
  nbPanelsPerPage,
16
  nbTotalPanelsToGenerate,
17
  oauthClientId: getValidString(process.env.HUGGING_FACE_OAUTH_CLIENT_ID, ""),
 
 
18
  oauthRedirectUrl: getValidString(process.env.HUGGING_FACE_OAUTH_REDIRECT_URL, ""),
 
19
  oauthScopes: "openid profile inference-api",
20
  enableHuggingFaceOAuth: getValidBoolean(process.env.ENABLE_HUGGING_FACE_OAUTH, false),
21
  enableHuggingFaceOAuthWall: getValidBoolean(process.env.ENABLE_HUGGING_FACE_OAUTH_WALL, false),
src/app/queries/getLLMEngineFunction.ts DELETED
@@ -1,19 +0,0 @@
1
- import { LLMEngine } from "@/types"
2
- import { predict as predictWithHuggingFace } from "./predictWithHuggingFace"
3
- import { predict as predictWithOpenAI } from "./predictWithOpenAI"
4
- import { predict as predictWithGroq } from "./predictWithGroq"
5
- import { predict as predictWithAnthropic } from "./predictWithAnthropic"
6
-
7
- export const defaultLLMEngineName = `${process.env.LLM_ENGINE || ""}` as LLMEngine
8
-
9
- export function getLLMEngineFunction(llmEngineName: LLMEngine = defaultLLMEngineName) {
10
- const llmEngineFunction =
11
- llmEngineName === "GROQ" ? predictWithGroq :
12
- llmEngineName === "ANTHROPIC" ? predictWithAnthropic :
13
- llmEngineName === "OPENAI" ? predictWithOpenAI :
14
- predictWithHuggingFace
15
-
16
- return llmEngineFunction
17
- }
18
-
19
- export const defaultLLMEngineFunction = getLLMEngineFunction()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/queries/getStoryContinuation.ts CHANGED
@@ -1,5 +1,5 @@
1
  import { Preset } from "../engine/presets"
2
- import { GeneratedPanel, LLMVendorConfig } from "@/types"
3
  import { predictNextPanels } from "./predictNextPanels"
4
  import { joinWords } from "@/lib/joinWords"
5
  import { sleep } from "@/lib/sleep"
@@ -11,7 +11,6 @@ export const getStoryContinuation = async ({
11
  nbPanelsToGenerate,
12
  maxNbPanels,
13
  existingPanels = [],
14
- llmVendorConfig
15
  }: {
16
  preset: Preset;
17
  stylePrompt?: string;
@@ -19,7 +18,6 @@ export const getStoryContinuation = async ({
19
  nbPanelsToGenerate: number;
20
  maxNbPanels: number;
21
  existingPanels?: GeneratedPanel[];
22
- llmVendorConfig: LLMVendorConfig
23
  }): Promise<GeneratedPanel[]> => {
24
 
25
  let panels: GeneratedPanel[] = []
@@ -36,7 +34,6 @@ export const getStoryContinuation = async ({
36
  nbPanelsToGenerate,
37
  maxNbPanels,
38
  existingPanels,
39
- llmVendorConfig,
40
  })
41
 
42
  // console.log("LLM responded with panelCandidates:", panelCandidates)
@@ -48,7 +45,6 @@ export const getStoryContinuation = async ({
48
  panels.push({
49
  panel: startAt + i,
50
  instructions: `${panelCandidates[i]?.instructions || ""}`,
51
- speech: `${panelCandidates[i]?.speech || ""}`,
52
  caption: `${panelCandidates[i]?.caption || ""}`,
53
  })
54
  }
@@ -65,7 +61,6 @@ export const getStoryContinuation = async ({
65
  userStoryPrompt,
66
  `${".".repeat(p)}`,
67
  ]),
68
- speech: "...",
69
  caption: "(Sorry, LLM generation failed: using degraded mode)"
70
  })
71
  }
 
1
  import { Preset } from "../engine/presets"
2
+ import { GeneratedPanel } from "@/types"
3
  import { predictNextPanels } from "./predictNextPanels"
4
  import { joinWords } from "@/lib/joinWords"
5
  import { sleep } from "@/lib/sleep"
 
11
  nbPanelsToGenerate,
12
  maxNbPanels,
13
  existingPanels = [],
 
14
  }: {
15
  preset: Preset;
16
  stylePrompt?: string;
 
18
  nbPanelsToGenerate: number;
19
  maxNbPanels: number;
20
  existingPanels?: GeneratedPanel[];
 
21
  }): Promise<GeneratedPanel[]> => {
22
 
23
  let panels: GeneratedPanel[] = []
 
34
  nbPanelsToGenerate,
35
  maxNbPanels,
36
  existingPanels,
 
37
  })
38
 
39
  // console.log("LLM responded with panelCandidates:", panelCandidates)
 
45
  panels.push({
46
  panel: startAt + i,
47
  instructions: `${panelCandidates[i]?.instructions || ""}`,
 
48
  caption: `${panelCandidates[i]?.caption || ""}`,
49
  })
50
  }
 
61
  userStoryPrompt,
62
  `${".".repeat(p)}`,
63
  ]),
 
64
  caption: "(Sorry, LLM generation failed: using degraded mode)"
65
  })
66
  }
src/app/queries/getSystemPrompt.ts CHANGED
@@ -19,9 +19,9 @@ export function getSystemPrompt({
19
  }) {
20
  return [
21
  `You are a writer specialized in ${preset.llmPrompt}`,
22
- `Please write detailed drawing instructions and short (2-3 sentences long) speeches and narrator captions for the ${firstNextOrLast} ${nbPanelsToGenerate} panels (out of ${maxNbPanels} in total) of a new story, but keep it open-ended (it will be continued and expanded later). Please make sure each of those ${nbPanelsToGenerate} panels include info about character gender, age, origin, clothes, colors, location, lights, etc. Speeches are the dialogues, so they MUST be written in 1st person style, and be short, eg a couple of short sentences. Only generate those ${nbPanelsToGenerate} panels, but take into account the fact the panels are part of a longer story (${maxNbPanels} panels long).`,
23
- `Give your response as a VALID JSON array like this: \`Array<{ panel: number; instructions: string; speech: string; caption: string; }>\`.`,
24
  // `Give your response as Markdown bullet points.`,
25
- `Be brief in the instructions, the speeches and the narrative captions of those ${nbPanelsToGenerate} panels, don't add your own comments. Write speeces in 1st person style, with intensity, humor etc. The speech must be captivating, smart, entertaining, usually a sentence or two. Be straight to the point, return JSON and never reply things like "Sure, I can.." etc. Reply using valid JSON!! Important: Write valid JSON!`
26
  ].filter(item => item).join("\n")
27
  }
 
19
  }) {
20
  return [
21
  `You are a writer specialized in ${preset.llmPrompt}`,
22
+ `Please write detailed drawing instructions and short (2-3 sentences long) speech captions for the ${firstNextOrLast} ${nbPanelsToGenerate} panels (out of ${maxNbPanels} in total) of a new story, but keep it open-ended (it will be continued and expanded later). Please make sure each of those ${nbPanelsToGenerate} panels include info about character gender, age, origin, clothes, colors, location, lights, etc. Only generate those ${nbPanelsToGenerate} panels, but take into account the fact the panels are part of a longer story (${maxNbPanels} panels long).`,
23
+ `Give your response as a VALID JSON array like this: \`Array<{ panel: number; instructions: string; caption: string; }>\`.`,
24
  // `Give your response as Markdown bullet points.`,
25
+ `Be brief in the instructions and narrative captions of those ${nbPanelsToGenerate} panels, don't add your own comments. The captions must be captivating, smart, entertaining. Be straight to the point, and never reply things like "Sure, I can.." etc. Reply using valid JSON!! Important: Write valid JSON!`
26
  ].filter(item => item).join("\n")
27
  }
src/app/queries/mockLLMResponse.ts CHANGED
@@ -3,49 +3,41 @@ import { GeneratedPanels } from "@/types"
3
  export const mockGeneratedPanels: GeneratedPanels = [{
4
  "panel": 1,
5
  "instructions": "wide shot of detective walking towards a UFO crash site",
6
- "speech": "Hmm.. interesting.",
7
  "caption": "Detective Jameson investigates a UFO crash in the desert"
8
  },
9
  {
10
  "panel": 2,
11
  "instructions": "close-up of detective's face, determined expression",
12
- "speech": "I've been tracking this case for weeks",
13
  "caption": "He's been tracking this case for weeks"
14
  },
15
  {
16
  "panel": 3,
17
  "instructions": "medium shot of detective examining UFO debris",
18
- "speech": "...",
19
  "caption": "The evidence is scattered all over the desert"
20
  },
21
  {
22
  "panel": 4,
23
  "instructions": "close-up of strange symbol on UFO debris",
24
- "speech": " what does this symbol mean?",
25
- "caption": "strange symbols"
26
  },
27
  {
28
  "panel": 5,
29
  "instructions": "wide shot of detective walking towards a strange rock formation",
30
- "speech": "I've been tracking this case for weeks",
31
  "caption": "Jameson follows a trail that leads him deeper into the desert"
32
  },
33
  {
34
  "panel": 6,
35
  "instructions": "medium shot of detective discovering an alien body",
36
- "speech": "I'm not alone in the desert",
37
- "caption": "He's not alone"
38
  },
39
  {
40
  "panel": 7,
41
  "instructions": "close-up of alien's face, eyes closed, peaceful expression",
42
- "speech": "...?",
43
  "caption": "An alien life form, deceased"
44
  },
45
  {
46
  "panel": 8,
47
  "instructions": "wide shot of detective standing over the alien body, looking up at the sky",
48
- "speech": "what other secrets lie beyond the stars?",
49
- "caption": "Jameson wonders"
50
  }
51
  ]
 
3
  export const mockGeneratedPanels: GeneratedPanels = [{
4
  "panel": 1,
5
  "instructions": "wide shot of detective walking towards a UFO crash site",
 
6
  "caption": "Detective Jameson investigates a UFO crash in the desert"
7
  },
8
  {
9
  "panel": 2,
10
  "instructions": "close-up of detective's face, determined expression",
 
11
  "caption": "He's been tracking this case for weeks"
12
  },
13
  {
14
  "panel": 3,
15
  "instructions": "medium shot of detective examining UFO debris",
 
16
  "caption": "The evidence is scattered all over the desert"
17
  },
18
  {
19
  "panel": 4,
20
  "instructions": "close-up of strange symbol on UFO debris",
21
+ "caption": "But what does this symbol mean?"
 
22
  },
23
  {
24
  "panel": 5,
25
  "instructions": "wide shot of detective walking towards a strange rock formation",
 
26
  "caption": "Jameson follows a trail that leads him deeper into the desert"
27
  },
28
  {
29
  "panel": 6,
30
  "instructions": "medium shot of detective discovering an alien body",
31
+ "caption": "He's not alone in the desert"
 
32
  },
33
  {
34
  "panel": 7,
35
  "instructions": "close-up of alien's face, eyes closed, peaceful expression",
 
36
  "caption": "An alien life form, deceased"
37
  },
38
  {
39
  "panel": 8,
40
  "instructions": "wide shot of detective standing over the alien body, looking up at the sky",
41
+ "caption": "Jameson wonders, what other secrets lie beyond the stars?"
 
42
  }
43
  ]
src/app/queries/predict.ts CHANGED
@@ -1,23 +1,15 @@
1
  "use server"
2
 
3
- import { LLMEngine, LLMPredictionFunctionParams } from "@/types"
4
- import { defaultLLMEngineName, getLLMEngineFunction } from "./getLLMEngineFunction"
5
-
6
- export async function predict(params: LLMPredictionFunctionParams): Promise<string> {
7
- const { llmVendorConfig: { vendor } } = params
8
- // LLMVendor = what the user configure in the UI (eg. a dropdown item called default server)
9
- // LLMEngine = the actual engine to use (eg. hugging face)
10
- const llmEngineName: LLMEngine =
11
- vendor === "ANTHROPIC" ? "ANTHROPIC" :
12
- vendor === "GROQ" ? "GROQ" :
13
- vendor === "OPENAI" ? "OPENAI" :
14
- defaultLLMEngineName
15
-
16
- const llmEngineFunction = getLLMEngineFunction(llmEngineName)
17
-
18
- // console.log("predict: using " + llmEngineName)
19
- const results = await llmEngineFunction(params)
20
-
21
- // console.log("predict: result: " + results)
22
- return results
23
- }
 
1
  "use server"
2
 
3
+ import { LLMEngine } from "@/types"
4
+ import { predict as predictWithHuggingFace } from "./predictWithHuggingFace"
5
+ import { predict as predictWithOpenAI } from "./predictWithOpenAI"
6
+ import { predict as predictWithGroq } from "./predictWithGroq"
7
+ import { predict as predictWithAnthropic } from "./predictWithAnthropic"
8
+
9
+ const llmEngine = `${process.env.LLM_ENGINE || ""}` as LLMEngine
10
+
11
+ export const predict =
12
+ llmEngine === "GROQ" ? predictWithGroq :
13
+ llmEngine === "ANTHROPIC" ? predictWithAnthropic :
14
+ llmEngine === "OPENAI" ? predictWithOpenAI :
15
+ predictWithHuggingFace
 
 
 
 
 
 
 
 
src/app/queries/predictNextPanels.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { GeneratedPanel, LLMVendorConfig } from "@/types"
2
  import { cleanJson } from "@/lib/cleanJson"
3
  import { dirtyGeneratedPanelCleaner } from "@/lib/dirtyGeneratedPanelCleaner"
4
  import { dirtyGeneratedPanelsParser } from "@/lib/dirtyGeneratedPanelsParser"
@@ -15,14 +15,12 @@ export const predictNextPanels = async ({
15
  nbPanelsToGenerate,
16
  maxNbPanels,
17
  existingPanels = [],
18
- llmVendorConfig,
19
  }: {
20
- preset: Preset
21
- prompt: string
22
- nbPanelsToGenerate: number
23
- maxNbPanels: number
24
- existingPanels: GeneratedPanel[]
25
- llmVendorConfig: LLMVendorConfig
26
  }): Promise<GeneratedPanel[]> => {
27
  // console.log("predictNextPanels: ", { prompt, nbPanelsToGenerate })
28
  // throw new Error("Planned maintenance")
@@ -31,7 +29,7 @@ export const predictNextPanels = async ({
31
  // return mockGeneratedPanels
32
 
33
  const existingPanelsTemplate = existingPanels.length
34
- ? ` To help you, here are the previous panels, their speeches and captions (note: if you see an anomaly here eg. no speech, no caption or the same description repeated multiple times, do not hesitate to fix the story): ${JSON.stringify(existingPanels, null, 2)}`
35
  : ''
36
 
37
  const firstNextOrLast =
@@ -55,20 +53,15 @@ export const predictNextPanels = async ({
55
 
56
  let result = ""
57
 
58
- // we don't require a lot of token for our task,
59
- // but to be safe, let's count ~200 tokens per panel
60
- const nbTokensPerPanel = 200
61
 
62
  const nbMaxNewTokens = nbPanelsToGenerate * nbTokensPerPanel
63
 
64
  try {
65
  // console.log(`calling predict:`, { systemPrompt, userPrompt, nbMaxNewTokens })
66
- result = `${await predict({
67
- systemPrompt,
68
- userPrompt,
69
- nbMaxNewTokens,
70
- llmVendorConfig
71
- })}`.trim()
72
  console.log("LLM result (1st trial):", result)
73
  if (!result.length) {
74
  throw new Error("empty result on 1st trial!")
@@ -79,12 +72,7 @@ export const predictNextPanels = async ({
79
  await sleep(2000)
80
 
81
  try {
82
- result = `${await predict({
83
- systemPrompt: systemPrompt + " \n ",
84
- userPrompt,
85
- nbMaxNewTokens,
86
- llmVendorConfig
87
- })}`.trim()
88
  console.log("LLM result (2nd trial):", result)
89
  if (!result.length) {
90
  throw new Error("empty result on 2nd trial!")
@@ -115,7 +103,6 @@ export const predictNextPanels = async ({
115
  .map((cap, i) => ({
116
  panel: i,
117
  caption: cap,
118
- speech: cap,
119
  instructions: cap,
120
  }))
121
  )
 
1
+ import { GeneratedPanel } from "@/types"
2
  import { cleanJson } from "@/lib/cleanJson"
3
  import { dirtyGeneratedPanelCleaner } from "@/lib/dirtyGeneratedPanelCleaner"
4
  import { dirtyGeneratedPanelsParser } from "@/lib/dirtyGeneratedPanelsParser"
 
15
  nbPanelsToGenerate,
16
  maxNbPanels,
17
  existingPanels = [],
 
18
  }: {
19
+ preset: Preset;
20
+ prompt: string;
21
+ nbPanelsToGenerate: number;
22
+ maxNbPanels: number;
23
+ existingPanels: GeneratedPanel[];
 
24
  }): Promise<GeneratedPanel[]> => {
25
  // console.log("predictNextPanels: ", { prompt, nbPanelsToGenerate })
26
  // throw new Error("Planned maintenance")
 
29
  // return mockGeneratedPanels
30
 
31
  const existingPanelsTemplate = existingPanels.length
32
+ ? ` To help you, here are the previous panels and their captions (note: if you see an anomaly here eg. no caption or the same description repeated multiple times, do not hesitate to fix the story): ${JSON.stringify(existingPanels, null, 2)}`
33
  : ''
34
 
35
  const firstNextOrLast =
 
53
 
54
  let result = ""
55
 
56
+ // we don't require a lot of token for our task
57
+ // but to be safe, let's count ~130 tokens per panel
58
+ const nbTokensPerPanel = 130
59
 
60
  const nbMaxNewTokens = nbPanelsToGenerate * nbTokensPerPanel
61
 
62
  try {
63
  // console.log(`calling predict:`, { systemPrompt, userPrompt, nbMaxNewTokens })
64
+ result = `${await predict({ systemPrompt, userPrompt, nbMaxNewTokens })}`.trim()
 
 
 
 
 
65
  console.log("LLM result (1st trial):", result)
66
  if (!result.length) {
67
  throw new Error("empty result on 1st trial!")
 
72
  await sleep(2000)
73
 
74
  try {
75
+ result = `${await predict({ systemPrompt: systemPrompt + " \n ", userPrompt, nbMaxNewTokens })}`.trim()
 
 
 
 
 
76
  console.log("LLM result (2nd trial):", result)
77
  if (!result.length) {
78
  throw new Error("empty result on 2nd trial!")
 
103
  .map((cap, i) => ({
104
  panel: i,
105
  caption: cap,
 
106
  instructions: cap,
107
  }))
108
  )
src/app/queries/predictWithAnthropic.ts CHANGED
@@ -1,6 +1,5 @@
1
  "use server"
2
 
3
- import { LLMPredictionFunctionParams } from '@/types';
4
  import Anthropic from '@anthropic-ai/sdk';
5
  import { MessageParam } from '@anthropic-ai/sdk/resources';
6
 
@@ -8,19 +7,13 @@ export async function predict({
8
  systemPrompt,
9
  userPrompt,
10
  nbMaxNewTokens,
11
- llmVendorConfig
12
- }: LLMPredictionFunctionParams): Promise<string> {
13
- const anthropicApiKey = `${
14
- llmVendorConfig.apiKey ||
15
- process.env.AUTH_ANTHROPIC_API_KEY ||
16
- ""
17
- }`
18
- const anthropicApiModel = `${
19
- llmVendorConfig.modelId ||
20
- process.env.LLM_ANTHROPIC_API_MODEL ||
21
- "claude-3-opus-20240229"
22
- }`
23
- if (!anthropicApiKey) { throw new Error(`cannot call Anthropic without an API key`) }
24
 
25
  const anthropic = new Anthropic({
26
  apiKey: anthropicApiKey,
@@ -40,7 +33,7 @@ export async function predict({
40
  max_tokens: nbMaxNewTokens,
41
  })
42
 
43
- return (res.content[0] as any)?.text || ""
44
  } catch (err) {
45
  console.error(`error during generation: ${err}`)
46
  return ""
 
1
  "use server"
2
 
 
3
  import Anthropic from '@anthropic-ai/sdk';
4
  import { MessageParam } from '@anthropic-ai/sdk/resources';
5
 
 
7
  systemPrompt,
8
  userPrompt,
9
  nbMaxNewTokens,
10
+ }: {
11
+ systemPrompt: string
12
+ userPrompt: string
13
+ nbMaxNewTokens: number
14
+ }): Promise<string> {
15
+ const anthropicApiKey = `${process.env.AUTH_ANTHROPIC_API_KEY || ""}`
16
+ const anthropicApiModel = `${process.env.LLM_ANTHROPIC_API_MODEL || "claude-3-opus-20240229"}`
 
 
 
 
 
 
17
 
18
  const anthropic = new Anthropic({
19
  apiKey: anthropicApiKey,
 
33
  max_tokens: nbMaxNewTokens,
34
  })
35
 
36
+ return res.content[0]?.text || ""
37
  } catch (err) {
38
  console.error(`error during generation: ${err}`)
39
  return ""
src/app/queries/predictWithGroq.ts CHANGED
@@ -1,26 +1,18 @@
1
  "use server"
2
 
3
- import { LLMPredictionFunctionParams } from "@/types"
4
  import Groq from "groq-sdk"
5
 
6
  export async function predict({
7
  systemPrompt,
8
  userPrompt,
9
  nbMaxNewTokens,
10
- llmVendorConfig
11
- }: LLMPredictionFunctionParams): Promise<string> {
12
- const groqApiKey = `${
13
- llmVendorConfig.apiKey ||
14
- process.env.AUTH_GROQ_API_KEY ||
15
- ""
16
- }`
17
- const groqApiModel = `${
18
- llmVendorConfig.modelId ||
19
- process.env.LLM_GROQ_API_MODEL ||
20
- "mixtral-8x7b-32768"
21
- }`
22
-
23
- if (!groqApiKey) { throw new Error(`cannot call Groq without an API key`) }
24
 
25
  const groq = new Groq({
26
  apiKey: groqApiKey,
 
1
  "use server"
2
 
 
3
  import Groq from "groq-sdk"
4
 
5
  export async function predict({
6
  systemPrompt,
7
  userPrompt,
8
  nbMaxNewTokens,
9
+ }: {
10
+ systemPrompt: string
11
+ userPrompt: string
12
+ nbMaxNewTokens: number
13
+ }): Promise<string> {
14
+ const groqApiKey = `${process.env.AUTH_GROQ_API_KEY || ""}`
15
+ const groqApiModel = `${process.env.LLM_GROQ_API_MODEL || "mixtral-8x7b-32768"}`
 
 
 
 
 
 
 
16
 
17
  const groq = new Groq({
18
  apiKey: groqApiKey,
src/app/queries/predictWithHuggingFace.ts CHANGED
@@ -1,16 +1,18 @@
1
  "use server"
2
 
3
  import { HfInference, HfInferenceEndpoint } from "@huggingface/inference"
4
- import { LLMEngine, LLMPredictionFunctionParams } from "@/types"
5
  import { createZephyrPrompt } from "@/lib/createZephyrPrompt"
6
 
7
  export async function predict({
8
  systemPrompt,
9
  userPrompt,
10
  nbMaxNewTokens,
11
- // llmVendorConfig // <-- arbitrary/custom LLM models hosted on HF is not supported yet using the UI
12
- }: LLMPredictionFunctionParams): Promise<string> {
13
-
 
 
14
  const hf = new HfInference(process.env.AUTH_HF_API_TOKEN)
15
 
16
  const llmEngine = `${process.env.LLM_ENGINE || ""}` as LLMEngine
 
1
  "use server"
2
 
3
  import { HfInference, HfInferenceEndpoint } from "@huggingface/inference"
4
+ import { LLMEngine } from "@/types"
5
  import { createZephyrPrompt } from "@/lib/createZephyrPrompt"
6
 
7
  export async function predict({
8
  systemPrompt,
9
  userPrompt,
10
  nbMaxNewTokens,
11
+ }: {
12
+ systemPrompt: string
13
+ userPrompt: string
14
+ nbMaxNewTokens: number
15
+ }): Promise<string> {
16
  const hf = new HfInference(process.env.AUTH_HF_API_TOKEN)
17
 
18
  const llmEngine = `${process.env.LLM_ENGINE || ""}` as LLMEngine
src/app/queries/predictWithOpenAI.ts CHANGED
@@ -2,30 +2,20 @@
2
 
3
  import type { ChatCompletionMessageParam } from "openai/resources/chat"
4
  import OpenAI from "openai"
5
- import { LLMPredictionFunctionParams } from "@/types"
6
 
7
  export async function predict({
8
  systemPrompt,
9
  userPrompt,
10
  nbMaxNewTokens,
11
- llmVendorConfig
12
- }: LLMPredictionFunctionParams): Promise<string> {
13
- const openaiApiKey = `${
14
- llmVendorConfig.apiKey ||
15
- process.env.AUTH_OPENAI_API_KEY ||
16
- ""
17
- }`
18
- const openaiApiModel = `${
19
- llmVendorConfig.modelId ||
20
- process.env.LLM_OPENAI_API_MODEL ||
21
- "gpt-4-turbo"
22
- }`
23
-
24
- if (!openaiApiKey) { throw new Error(`cannot call OpenAI without an API key`) }
25
-
26
-
27
  const openaiApiBaseUrl = `${process.env.LLM_OPENAI_API_BASE_URL || "https://api.openai.com/v1"}`
28
-
 
29
  const openai = new OpenAI({
30
  apiKey: openaiApiKey,
31
  baseURL: openaiApiBaseUrl,
 
2
 
3
  import type { ChatCompletionMessageParam } from "openai/resources/chat"
4
  import OpenAI from "openai"
 
5
 
6
  export async function predict({
7
  systemPrompt,
8
  userPrompt,
9
  nbMaxNewTokens,
10
+ }: {
11
+ systemPrompt: string
12
+ userPrompt: string
13
+ nbMaxNewTokens: number
14
+ }): Promise<string> {
15
+ const openaiApiKey = `${process.env.AUTH_OPENAI_API_KEY || ""}`
 
 
 
 
 
 
 
 
 
 
16
  const openaiApiBaseUrl = `${process.env.LLM_OPENAI_API_BASE_URL || "https://api.openai.com/v1"}`
17
+ const openaiApiModel = `${process.env.LLM_OPENAI_API_MODEL || "gpt-3.5-turbo"}`
18
+
19
  const openai = new OpenAI({
20
  apiKey: openaiApiKey,
21
  baseURL: openaiApiBaseUrl,
src/app/store/index.ts CHANGED
@@ -1,24 +1,17 @@
1
  "use client"
2
 
3
  import { create } from "zustand"
4
- import { ClapProject, ClapImageRatio, ClapSegment, ClapSegmentCategory, ClapSegmentStatus, ClapOutputType, 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
- import { parsePresetFromPrompts } from "@/lib/parsePresetFromPrompts"
14
- import { parseLayoutFromStoryboards } from "@/lib/parseLayoutFromStoryboards"
15
- import { getLocalStorageShowSpeeches } from "@/lib/getLocalStorageShowSpeeches"
16
 
17
  export const useStore = create<{
18
  prompt: string
19
  font: FontName
20
  preset: Preset
21
- currentClap?: ClapProject
22
  currentNbPanelsPerPage: number
23
  maxNbPanelsPerPage: number
24
  currentNbPages: number
@@ -27,10 +20,8 @@ export const useStore = create<{
27
  currentNbPanels: number
28
  maxNbPanels: number
29
  panels: string[]
30
- speeches: string[]
31
  captions: string[]
32
  upscaleQueue: Record<string, RenderedScene>
33
- showSpeeches: boolean
34
  showCaptions: boolean
35
  renderedScenes: Record<string, RenderedScene>
36
  layout: LayoutName
@@ -58,12 +49,9 @@ export const useStore = create<{
58
  setPreset: (preset: Preset) => void
59
  setPanels: (panels: string[]) => void
60
  setPanelPrompt: (newPrompt: string, index: number) => void
61
- setLayout: (layout: LayoutName, index?: number) => void
62
- setLayouts: (layouts: LayoutName[]) => void
63
- setShowSpeeches: (showSpeeches: boolean) => void
64
- setSpeeches: (speeches: string[]) => void
65
- setPanelSpeech: (newSpeech: string, index: number) => void
66
  setShowCaptions: (showCaptions: boolean) => void
 
 
67
  setCaptions: (captions: string[]) => void
68
  setPanelCaption: (newCaption: string, index: number) => void
69
  setZoomLevel: (zoomLevel: number) => void
@@ -81,57 +69,31 @@ export const useStore = create<{
81
  // setPage: (page: HTMLDivElement) => void
82
 
83
  generate: (prompt: string, presetName: PresetName, layoutName: LayoutName) => void
84
- convertComicToClap: () => Promise<ClapProject>
85
- convertClapToComic: (clap: ClapProject) => Promise<{
86
- currentNbPanels: number
87
- prompt: string
88
- preset: Preset
89
- layout: LayoutName
90
- storyPrompt: string
91
- stylePrompt: string
92
- panels: string[]
93
- renderedScenes: Record<string, RenderedScene>
94
- speeches: string[]
95
- captions: string[]
96
- }>
97
- loadClap: (blob: Blob) => Promise<void>
98
- downloadClap: () => Promise<void>
99
  }>((set, get) => ({
100
-
101
- // -------- note --------------------------------------------------
102
- // do not read the local storage in this block, results might be empty
103
- // ----------------------------------------------------------------
104
-
105
- prompt:
106
- (getParam("stylePrompt", "") || getParam("storyPrompt", ""))
107
- ? `${getParam("stylePrompt", "")}||${getParam("storyPrompt", "")}`
108
- : "",
109
  font: "actionman",
110
- preset: getPreset(getParam("preset", defaultPreset)),
111
 
112
- currentClap: undefined,
113
  currentNbPanelsPerPage: 4,
114
  maxNbPanelsPerPage: 4,
115
  currentNbPages: 1,
116
- maxNbPages: getParam("maxNbPages", 1),
117
  previousNbPanels: 0,
118
  currentNbPanels: 4,
119
  maxNbPanels: 4,
120
 
121
  panels: [],
122
- speeches: [],
123
  captions: [],
124
  upscaleQueue: {} as Record<string, RenderedScene>,
125
  renderedScenes: {} as Record<string, RenderedScene>,
126
- showSpeeches: true,
127
- showCaptions: getParam("showCaptions", false),
128
 
129
  // deprecated?
130
  layout: defaultLayout,
131
 
132
  layouts: [defaultLayout, defaultLayout, defaultLayout, defaultLayout],
133
 
134
- zoomLevel: getParam("zoomLevel", 60),
135
 
136
  // deprecated?
137
  page: undefined as unknown as HTMLDivElement,
@@ -298,29 +260,6 @@ export const useStore = create<{
298
  ))
299
  })
300
  },
301
- setSpeeches: (speeches: string[]) => {
302
- set({
303
- speeches,
304
- })
305
- },
306
- setShowSpeeches: (showSpeeches: boolean) => {
307
- set({
308
- showSpeeches,
309
- })
310
- try {
311
- localStorage.setItem("AI_COMIC_FACTORY_SHOW_SPEECHES", `${showSpeeches || false}`)
312
- } catch (err) {
313
- console.error(`failed to persist "showSpeeches" for value "${showSpeeches}"`)
314
- }
315
- },
316
- setPanelSpeech: (newSpeech, index) => {
317
- const { speeches } = get()
318
- set({
319
- speeches: speeches.map((c, i) => (
320
- index === i ? newSpeech : c
321
- ))
322
- })
323
- },
324
  setCaptions: (captions: string[]) => {
325
  set({
326
  captions,
@@ -339,19 +278,15 @@ export const useStore = create<{
339
  ))
340
  })
341
  },
342
- setLayout: (layoutName: LayoutName, index?: number) => {
343
- const { maxNbPages, currentNbPanelsPerPage, layouts } = get()
344
 
 
345
  for (let i = 0; i < maxNbPages; i++) {
346
- let name = layoutName === "random" ? getRandomLayoutName() : layoutName
347
-
348
- if (typeof index === "number" && !isNaN(index) && isFinite(index)) {
349
- if (i === index) {
350
- layouts[i] = name
351
- }
352
- } else {
353
- layouts[i] = name
354
- }
355
  }
356
 
357
  set({
@@ -361,7 +296,6 @@ export const useStore = create<{
361
  currentNbPages: 1,
362
  currentNbPanels: currentNbPanelsPerPage,
363
  panels: [],
364
- speeches: [],
365
  captions: [],
366
  upscaleQueue: {},
367
  renderedScenes: {},
@@ -446,7 +380,6 @@ export const useStore = create<{
446
  currentNbPages: 1,
447
  currentNbPanels: currentNbPanelsPerPage,
448
  panels: [],
449
- speeches: [],
450
  captions: [],
451
  upscaleQueue: {},
452
  renderedScenes: {},
@@ -462,270 +395,5 @@ export const useStore = create<{
462
  layout: layouts[0],
463
  layouts,
464
  })
465
- },
466
-
467
- convertComicToClap: async (): Promise<ClapProject> => {
468
- const {
469
- currentNbPanels,
470
- prompt,
471
- panels,
472
- renderedScenes,
473
- speeches,
474
- captions
475
- } = get()
476
-
477
- const defaultSegmentDurationInMs = 7000
478
-
479
- let currentElapsedTimeInMs = 0
480
-
481
-
482
- const clap: ClapProject = newClap({
483
- meta: {
484
- title: "Untitled", // we don't need a title actually
485
- description: prompt,
486
- storyPrompt: prompt,
487
- imagePrompt: "",
488
- systemPrompt: "",
489
- synopsis: "",
490
- licence: "",
491
- imageRatio: ClapImageRatio.LANDSCAPE,
492
- width: 512,
493
- height: 288,
494
- isInteractive: false,
495
- isLoop: false,
496
- durationInMs: panels.length * defaultSegmentDurationInMs,
497
- bpm: 1,
498
- frameRate: 1,
499
- }
500
- })
501
-
502
- for (let i = 0; i < panels.length; i++) {
503
-
504
- const panel = panels[i]
505
- const speech = speeches[i]
506
- const caption = captions[i]
507
-
508
- const renderedScene = renderedScenes[`${i}`]
509
-
510
- clap.segments.push(newSegment({
511
- track: 1,
512
- startTimeInMs: currentElapsedTimeInMs,
513
- assetDurationInMs: defaultSegmentDurationInMs,
514
- category: ClapSegmentCategory.IMAGE,
515
- prompt: panel,
516
- outputType: ClapOutputType.IMAGE,
517
- assetUrl: renderedScene?.assetUrl || "",
518
- status: ClapSegmentStatus.COMPLETED,
519
- }))
520
-
521
- clap.segments.push(newSegment({
522
- track: 2,
523
- startTimeInMs: currentElapsedTimeInMs,
524
- assetDurationInMs: defaultSegmentDurationInMs,
525
- category: ClapSegmentCategory.INTERFACE,
526
- prompt: caption,
527
- // assetUrl: `data:text/plain;base64,${btoa(title)}`,
528
- assetUrl: caption,
529
- outputType: ClapOutputType.TEXT,
530
- status: ClapSegmentStatus.COMPLETED,
531
- }))
532
-
533
- clap.segments.push(newSegment({
534
- track: 3,
535
- startTimeInMs: currentElapsedTimeInMs,
536
- assetDurationInMs: defaultSegmentDurationInMs,
537
- category: ClapSegmentCategory.DIALOGUE,
538
- prompt: speech,
539
- outputType: ClapOutputType.AUDIO,
540
- status: ClapSegmentStatus.TO_GENERATE,
541
- }))
542
-
543
- // the presence of a camera is mandatory
544
- clap.segments.push(newSegment({
545
- track: 4,
546
- startTimeInMs: currentElapsedTimeInMs,
547
- assetDurationInMs: defaultSegmentDurationInMs,
548
- category: ClapSegmentCategory.CAMERA,
549
- prompt: "movie still",
550
- outputType: ClapOutputType.TEXT,
551
- status: ClapSegmentStatus.COMPLETED,
552
- }))
553
-
554
- currentElapsedTimeInMs += defaultSegmentDurationInMs
555
- }
556
-
557
- set({ currentClap: clap })
558
-
559
- return clap
560
- },
561
-
562
- convertClapToComic: async (clap: ClapProject): Promise<{
563
- currentNbPanels: number
564
- prompt: string
565
- preset: Preset
566
- layout: LayoutName
567
- storyPrompt: string
568
- stylePrompt: string
569
- panels: string[]
570
- renderedScenes: Record<string, RenderedScene>
571
- speeches: string[]
572
- captions: string[]
573
- }> => {
574
-
575
- const prompt = clap.meta.description
576
- const [stylePrompt, storyPrompt] = prompt.split("||").map(x => x.trim())
577
-
578
- const panels: string[] = []
579
- const renderedScenes: Record<string, RenderedScene> = {}
580
- const captions: string[] = []
581
- const speeches: string[] = []
582
-
583
- const panelGenerationStatus: Record<number, boolean> = {}
584
-
585
- const cameraShots = clap.segments.filter(s => s.category === ClapSegmentCategory.CAMERA)
586
-
587
- const shots = cameraShots.map(cameraShot => ({
588
- camera: cameraShot,
589
- storyboard: filterSegments(
590
- ClapSegmentFilteringMode.START,
591
- cameraShot,
592
- clap.segments,
593
- ClapSegmentCategory.IMAGE,
594
- ).at(0) as (ClapSegment | undefined),
595
- ui: filterSegments(
596
- ClapSegmentFilteringMode.START,
597
- cameraShot,
598
- clap.segments,
599
- ClapSegmentCategory.INTERFACE,
600
- ).at(0) as (ClapSegment | undefined),
601
- dialogue: filterSegments(
602
- ClapSegmentFilteringMode.START,
603
- cameraShot,
604
- clap.segments,
605
- ClapSegmentCategory.DIALOGUE,
606
- ).at(0) as (ClapSegment | undefined)
607
- })).filter(item => item.storyboard && item.ui) as {
608
- camera: ClapSegment
609
- storyboard: ClapSegment
610
- ui: ClapSegment
611
- dialogue: ClapSegment
612
- }[]
613
-
614
- shots.forEach(({ camera, storyboard, ui, dialogue }, id) => {
615
-
616
- panels.push(storyboard.prompt)
617
-
618
- const renderedScene: RenderedScene = {
619
- renderId: storyboard?.id || "",
620
- status: "pending",
621
- assetUrl: "",
622
- alt: storyboard?.prompt || "",
623
- error: "",
624
- maskUrl: "",
625
- segments: []
626
- }
627
-
628
- if (storyboard?.assetUrl) {
629
- renderedScene.assetUrl = storyboard.assetUrl
630
- renderedScene.status = "pregenerated" // <- special trick to indicate that it should not be re-generated
631
- }
632
-
633
- renderedScenes[id] = renderedScene
634
-
635
- panelGenerationStatus[id] = false
636
-
637
- speeches.push(dialogue?.prompt || "")
638
-
639
- captions.push(ui?.prompt || "")
640
- })
641
-
642
-
643
- return {
644
- currentNbPanels: shots.length,
645
- prompt,
646
- preset: parsePresetFromPrompts(panels),
647
- layout: await parseLayoutFromStoryboards(shots.map(x => x.storyboard)),
648
- storyPrompt,
649
- stylePrompt,
650
- panels,
651
- renderedScenes,
652
- speeches,
653
- captions,
654
-
655
- }
656
- },
657
-
658
- loadClap: async (blob: Blob) => {
659
- const { convertClapToComic, currentNbPanelsPerPage } = get()
660
-
661
- const currentClap = await parseClap(blob)
662
-
663
- const {
664
- currentNbPanels,
665
- prompt,
666
- preset,
667
- layout,
668
- storyPrompt,
669
- stylePrompt,
670
- panels,
671
- renderedScenes,
672
- speeches,
673
- captions,
674
- } = await convertClapToComic(currentClap)
675
-
676
- // kids, don't do this in your projects: use state managers instead!
677
- putTextInInput(document.getElementById("top-menu-input-style-prompt") as HTMLInputElement, stylePrompt)
678
- putTextInInput(document.getElementById("top-menu-input-story-prompt") as HTMLInputElement, storyPrompt)
679
-
680
- set({
681
- currentClap,
682
- currentNbPanels,
683
- prompt,
684
- preset,
685
- // layout,
686
- panels,
687
- renderedScenes,
688
- speeches,
689
- captions,
690
- currentNbPages: Math.round(currentNbPanels / currentNbPanelsPerPage),
691
- upscaleQueue: {},
692
- isGeneratingStory: false,
693
- isGeneratingText: false,
694
- })
695
- },
696
-
697
- downloadClap: async () => {
698
- const { convertComicToClap, prompt } = get()
699
-
700
- const currentClap = await convertComicToClap()
701
-
702
- if (!currentClap) { throw new Error(`cannot save a clap.. if there is no clap`) }
703
-
704
- const currentClapBlob: Blob = await serializeClap(currentClap)
705
-
706
- // Create an object URL for the compressed clap blob
707
- const objectUrl = URL.createObjectURL(currentClapBlob)
708
-
709
- // Create an anchor element and force browser download
710
- const anchor = document.createElement("a")
711
- anchor.href = objectUrl
712
-
713
- const [stylePrompt, storyPrompt] = prompt.split("||").map(x => x.trim())
714
-
715
- const cleanStylePrompt = (stylePrompt || "").replace(/([^a-z0-9, ]+)/gi, " ")
716
-
717
- const firstPartOfStory = (storyPrompt || "").split(",").shift() || ""
718
- const cleanStoryPrompt = firstPartOfStory.replace(/([^a-z0-9, ]+)/gi, " ")
719
-
720
- const cleanName = `${cleanStoryPrompt.slice(0, 90)} (${cleanStylePrompt.slice(0, 90) || "default style"})`
721
-
722
- anchor.download = `${cleanName}.clap`
723
-
724
- document.body.appendChild(anchor) // Append to the body (could be removed once clicked)
725
- anchor.click() // Trigger the download
726
-
727
- // Cleanup: revoke the object URL and remove the anchor element
728
- URL.revokeObjectURL(objectUrl)
729
- document.body.removeChild(anchor)
730
- },
731
  }))
 
1
  "use client"
2
 
3
  import { create } from "zustand"
4
+ import html2canvas from "html2canvas"
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
 
11
  export const useStore = create<{
12
  prompt: string
13
  font: FontName
14
  preset: Preset
 
15
  currentNbPanelsPerPage: number
16
  maxNbPanelsPerPage: number
17
  currentNbPages: number
 
20
  currentNbPanels: number
21
  maxNbPanels: number
22
  panels: string[]
 
23
  captions: string[]
24
  upscaleQueue: Record<string, RenderedScene>
 
25
  showCaptions: boolean
26
  renderedScenes: Record<string, RenderedScene>
27
  layout: LayoutName
 
49
  setPreset: (preset: Preset) => void
50
  setPanels: (panels: string[]) => void
51
  setPanelPrompt: (newPrompt: string, index: number) => void
 
 
 
 
 
52
  setShowCaptions: (showCaptions: boolean) => void
53
+ setLayout: (layout: LayoutName) => void
54
+ setLayouts: (layouts: LayoutName[]) => void
55
  setCaptions: (captions: string[]) => void
56
  setPanelCaption: (newCaption: string, index: number) => void
57
  setZoomLevel: (zoomLevel: number) => void
 
69
  // setPage: (page: HTMLDivElement) => void
70
 
71
  generate: (prompt: string, presetName: PresetName, layoutName: LayoutName) => void
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  }>((set, get) => ({
73
+ prompt: "",
 
 
 
 
 
 
 
 
74
  font: "actionman",
75
+ preset: getPreset(defaultPreset),
76
 
 
77
  currentNbPanelsPerPage: 4,
78
  maxNbPanelsPerPage: 4,
79
  currentNbPages: 1,
80
+ maxNbPages: 1,
81
  previousNbPanels: 0,
82
  currentNbPanels: 4,
83
  maxNbPanels: 4,
84
 
85
  panels: [],
 
86
  captions: [],
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,
 
260
  ))
261
  })
262
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  setCaptions: (captions: string[]) => {
264
  set({
265
  captions,
 
278
  ))
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({
 
296
  currentNbPages: 1,
297
  currentNbPanels: currentNbPanelsPerPage,
298
  panels: [],
 
299
  captions: [],
300
  upscaleQueue: {},
301
  renderedScenes: {},
 
380
  currentNbPages: 1,
381
  currentNbPanels: currentNbPanelsPerPage,
382
  panels: [],
 
383
  captions: [],
384
  upscaleQueue: {},
385
  renderedScenes: {},
 
395
  layout: layouts[0],
396
  layouts,
397
  })
398
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  }))
src/lib/bubble/injectSpeechBubbleInTheBackground.ts DELETED
@@ -1,543 +0,0 @@
1
- import { ImageSegmenter, FilesetResolver, ImageSegmenterResult } from "@mediapipe/tasks-vision"
2
- import { actionman } from "../fonts";
3
-
4
- interface BoundingBox {
5
- top: number;
6
- left: number;
7
- width: number;
8
- height: number;
9
- }
10
-
11
- /**
12
- * Injects speech bubbles into the background of an image.
13
- * @param params - The parameters for injecting speech bubbles.
14
- * @returns A Promise that resolves to a base64-encoded string of the modified image.
15
- */
16
- export async function injectSpeechBubbleInTheBackground(params: {
17
- inputImageInBase64: string;
18
- text?: string;
19
- shape?: "oval" | "rectangular" | "cloud" | "thought";
20
- line?: "handdrawn" | "straight" | "bubble" | "chaotic";
21
- font?: string;
22
- debug?: boolean;
23
- }): Promise<string> {
24
- const {
25
- inputImageInBase64,
26
- text,
27
- shape = "oval",
28
- line = "handdrawn",
29
- font = actionman.style.fontFamily,
30
- debug = false,
31
- } = params;
32
-
33
- if (!text) {
34
- return inputImageInBase64;
35
- }
36
-
37
- const image = await loadImage(inputImageInBase64);
38
- const canvas = document.createElement('canvas');
39
- canvas.width = image.width;
40
- canvas.height = image.height;
41
- const ctx = canvas.getContext('2d')!;
42
- ctx.drawImage(image, 0, 0);
43
-
44
- const vision = await FilesetResolver.forVisionTasks(
45
- "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm"
46
- );
47
- const imageSegmenter = await ImageSegmenter.createFromOptions(vision, {
48
- baseOptions: {
49
- modelAssetPath: "https://storage.googleapis.com/mediapipe-models/image_segmenter/deeplab_v3/float32/1/deeplab_v3.tflite",
50
- delegate: "GPU"
51
- },
52
- outputCategoryMask: true,
53
- outputConfidenceMasks: false
54
- });
55
-
56
- const segmentationResult: ImageSegmenterResult = imageSegmenter.segment(image);
57
- let characterBoundingBox: BoundingBox | null = null;
58
-
59
- if (segmentationResult.categoryMask) {
60
- const mask = segmentationResult.categoryMask.getAsUint8Array();
61
- characterBoundingBox = findCharacterBoundingBox(mask, image.width, image.height);
62
- console.log(segmentationResult)
63
- if (debug) {
64
- drawSegmentationMask(ctx, mask, image.width, image.height);
65
- }
66
- }
67
-
68
- const bubbles = splitTextIntoBubbles(text);
69
- const bubbleLocations = calculateBubbleLocations(bubbles.length, image.width, image.height, characterBoundingBox);
70
-
71
- bubbles.forEach((bubbleText, index) => {
72
- const bubbleLocation = bubbleLocations[index];
73
- drawSpeechBubble(ctx, bubbleLocation, bubbleText, shape, line, font, characterBoundingBox, image.width, image.height);
74
- });
75
-
76
- return canvas.toDataURL('image/png');
77
- }
78
-
79
- function loadImage(base64: string): Promise<HTMLImageElement> {
80
- return new Promise((resolve, reject) => {
81
- const img = new Image();
82
- img.onload = () => resolve(img);
83
- img.onerror = reject;
84
- img.src = base64;
85
- });
86
- }
87
-
88
- function findCharacterBoundingBox(mask: Uint8Array, width: number, height: number): BoundingBox | null {
89
- let shapes: BoundingBox[] = [];
90
- let visited = new Set<number>();
91
-
92
- for (let y = 0; y < height; y++) {
93
- for (let x = 0; x < width; x++) {
94
- const index = y * width + x;
95
- if (mask[index] > 0 && !visited.has(index)) {
96
- let shape = floodFill(mask, width, height, x, y, visited);
97
- shapes.push(shape);
98
- }
99
- }
100
- }
101
-
102
- // Sort shapes by area (descending) and filter out small shapes
103
- shapes = shapes
104
- .filter(shape => (shape.width * shape.height) > (width * height * 0.01))
105
- .sort((a, b) => (b.width * b.height) - (a.width * a.height));
106
-
107
- // Find the most vertically rectangular shape
108
- let mostVerticalShape = shapes.reduce((prev, current) => {
109
- let prevRatio = prev.height / prev.width;
110
- let currentRatio = current.height / current.width;
111
- return currentRatio > prevRatio ? current : prev;
112
- });
113
-
114
- return mostVerticalShape || null;
115
- }
116
-
117
- function floodFill(mask: Uint8Array, width: number, height: number, startX: number, startY: number, visited: Set<number>): BoundingBox {
118
- let queue = [[startX, startY]];
119
- let minX = startX, maxX = startX, minY = startY, maxY = startY;
120
-
121
- while (queue.length > 0) {
122
- let [x, y] = queue.pop()!;
123
- let index = y * width + x;
124
-
125
- if (x < 0 || x >= width || y < 0 || y >= height || mask[index] === 0 || visited.has(index)) {
126
- continue;
127
- }
128
-
129
- visited.add(index);
130
- minX = Math.min(minX, x);
131
- maxX = Math.max(maxX, x);
132
- minY = Math.min(minY, y);
133
- maxY = Math.max(maxY, y);
134
-
135
- queue.push([x+1, y], [x-1, y], [x, y+1], [x, y-1]);
136
- }
137
-
138
- return {
139
- left: minX,
140
- top: minY,
141
- width: maxX - minX + 1,
142
- height: maxY - minY + 1
143
- };
144
- }
145
-
146
- function analyzeSegmentationMask(mask: Uint8Array, width: number, height: number): string[] {
147
- const categories = new Set<number>();
148
- for (let i = 0; i < mask.length; i++) {
149
- if (mask[i] > 0) {
150
- categories.add(mask[i]);
151
- }
152
- }
153
- return Array.from(categories).map(c => `unknown-${c}`);
154
- }
155
-
156
- function splitTextIntoBubbles(text: string): string[] {
157
- // Define a regular expression pattern
158
- const pattern = /(?:[A-Z][a-z]*\.\s*)*(?:[^.!?\s]+[^.!?]*[.!?]+)|\S+/g;
159
-
160
- const matches = text.match(pattern) || [text];
161
- return matches.map(sentence => sentence.trim());
162
- }
163
-
164
- function calculateBubbleLocations(
165
- bubbleCount: number,
166
- imageWidth: number,
167
- imageHeight: number,
168
- characterBoundingBox: BoundingBox | null
169
- ): { x: number, y: number }[] {
170
- const locations: { x: number, y: number }[] = [];
171
- const padding = 50;
172
- const availableWidth = imageWidth - padding * 2;
173
- const availableHeight = imageHeight - padding * 2;
174
- const maxAttempts = 100;
175
-
176
- for (let i = 0; i < bubbleCount; i++) {
177
- let x, y;
178
- let attempts = 0;
179
- do {
180
- // Adjust x to avoid the middle of the character
181
- if (characterBoundingBox) {
182
- const characterMiddle = characterBoundingBox.left + characterBoundingBox.width / 2;
183
- const leftSide = Math.random() * (characterMiddle - padding - padding);
184
- const rightSide = characterMiddle + Math.random() * (imageWidth - characterMiddle - padding - padding);
185
- x = Math.random() < 0.5 ? leftSide : rightSide;
186
- } else {
187
- x = Math.random() * availableWidth + padding;
188
- }
189
- y = (i / bubbleCount) * availableHeight + padding;
190
- attempts++;
191
-
192
- if (attempts >= maxAttempts) {
193
- console.warn(`Could not find non-overlapping position for bubble ${i} after ${maxAttempts} attempts.`);
194
- break;
195
- }
196
- } while (characterBoundingBox && isOverlapping({ x, y }, characterBoundingBox));
197
-
198
- locations.push({ x, y });
199
- }
200
-
201
- return locations;
202
- }
203
-
204
- function isOverlapping(point: { x: number, y: number }, box: BoundingBox): boolean {
205
- return point.x >= box.left && point.x <= box.left + box.width &&
206
- point.y >= box.top && point.y <= box.top + box.height;
207
- }
208
-
209
- function drawSegmentationMask(ctx: CanvasRenderingContext2D, mask: Uint8Array, width: number, height: number) {
210
- const imageData = ctx.getImageData(0, 0, width, height);
211
- const data = imageData.data;
212
- for (let i = 0; i < mask.length; i++) {
213
- const category = mask[i];
214
- if (category > 0) {
215
- // Use a different color for each category
216
- const color = getCategoryColor(category);
217
- data[i * 4] = color[0];
218
- data[i * 4 + 1] = color[1];
219
- data[i * 4 + 2] = color[2];
220
- data[i * 4 + 3] = 128; // 50% opacity
221
- }
222
- }
223
- ctx.putImageData(imageData, 0, 0);
224
- }
225
-
226
- function getCategoryColor(category: number): [number, number, number] {
227
- // Generate a pseudo-random color based on the category
228
- const hue = (category * 137) % 360;
229
- return hslToRgb(hue / 360, 1, 0.5);
230
- }
231
-
232
- function hslToRgb(h: number, s: number, l: number): [number, number, number] {
233
- let r, g, b;
234
- if (s === 0) {
235
- r = g = b = l;
236
- } else {
237
- const hue2rgb = (p: number, q: number, t: number) => {
238
- if (t < 0) t += 1;
239
- if (t > 1) t -= 1;
240
- if (t < 1/6) return p + (q - p) * 6 * t;
241
- if (t < 1/2) return q;
242
- if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
243
- return p;
244
- };
245
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
246
- const p = 2 * l - q;
247
- r = hue2rgb(p, q, h + 1/3);
248
- g = hue2rgb(p, q, h);
249
- b = hue2rgb(p, q, h - 1/3);
250
- }
251
- return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
252
- }
253
-
254
- function drawSpeechBubble(
255
- ctx: CanvasRenderingContext2D,
256
- location: { x: number; y: number },
257
- text: string,
258
- shape: "oval" | "rectangular" | "cloud" | "thought",
259
- line: "handdrawn" | "straight" | "bubble" | "chaotic",
260
- font: string,
261
- characterBoundingBox: BoundingBox | null,
262
- imageWidth: number,
263
- imageHeight: number,
264
- safetyMargin: number = 0.1 // Default safety margin is 10%
265
- ) {
266
- const padding = 24;
267
- const borderPadding = Math.max(10, Math.min(imageWidth, imageHeight) * safetyMargin);
268
-
269
- const fontSize = 20;
270
- ctx.font = `${fontSize}px ${font}`;
271
-
272
- // Adjust maximum width to account for border padding and limit to 33% of image width
273
- const maxBubbleWidth = Math.min(imageWidth - 2 * borderPadding, imageWidth * 0.33);
274
- const wrappedText = wrapText(ctx, text, maxBubbleWidth - padding * 2, fontSize);
275
- const textDimensions = measureTextDimensions(ctx, wrappedText, fontSize);
276
-
277
- // Adjust bubble size based on text content
278
- const finalWidth = Math.min(Math.max(textDimensions.width + padding * 2, 100), maxBubbleWidth);
279
- const finalHeight = Math.min(Math.max(textDimensions.height + padding * 2, 50), imageHeight - 2 * borderPadding);
280
-
281
- const bubbleLocation = adjustBubbleLocation(location, finalWidth, finalHeight, characterBoundingBox, imageWidth, imageHeight, borderPadding);
282
-
283
- let tailTarget = null;
284
- if (characterBoundingBox) {
285
- tailTarget = {
286
- x: characterBoundingBox.left + characterBoundingBox.width / 2,
287
- y: characterBoundingBox.top + characterBoundingBox.height * 0.3
288
- };
289
- }
290
-
291
- // Draw the main bubble
292
- ctx.fillStyle = 'white';
293
- ctx.strokeStyle = 'black';
294
- ctx.lineWidth = 2;
295
- ctx.beginPath();
296
- drawBubbleShape(ctx, shape, bubbleLocation, finalWidth, finalHeight, tailTarget);
297
- ctx.fill();
298
- ctx.stroke();
299
-
300
- // Draw the tail
301
- if (tailTarget) {
302
- drawTail(ctx, bubbleLocation, finalWidth, finalHeight, tailTarget, shape);
303
- }
304
-
305
- // Draw a white oval to blend the tail with the bubble
306
- ctx.fillStyle = 'white';
307
- ctx.beginPath();
308
- drawBubbleShape(ctx, shape, bubbleLocation, finalWidth, finalHeight, null);
309
- ctx.fill();
310
-
311
- // Draw the text
312
- ctx.fillStyle = 'black';
313
- ctx.textAlign = 'center';
314
- ctx.textBaseline = 'middle';
315
- drawFormattedText(ctx, wrappedText, bubbleLocation.x, bubbleLocation.y, finalWidth - padding * 2, fontSize);
316
- }
317
-
318
- function drawTail(
319
- ctx: CanvasRenderingContext2D,
320
- bubbleLocation: { x: number; y: number },
321
- bubbleWidth: number,
322
- bubbleHeight: number,
323
- tailTarget: { x: number; y: number },
324
- shape: string
325
- ) {
326
- const bubbleCenterX = bubbleLocation.x;
327
- const bubbleCenterY = bubbleLocation.y;
328
- const tailBaseWidth = 40;
329
-
330
- // Calculate the distance from bubble center to tail target
331
- const deltaX = tailTarget.x - bubbleCenterX;
332
- const deltaY = tailTarget.y - bubbleCenterY;
333
- const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
334
-
335
- // Set the tail length to 30% of the distance
336
- const tailLength = distance * 0.3;
337
-
338
- // Calculate the tail end point
339
- const tailEndX = bubbleCenterX + (deltaX / distance) * tailLength;
340
- const tailEndY = bubbleCenterY + (deltaY / distance) * tailLength;
341
-
342
- // Calculate the angle of the tail
343
- const angle = Math.atan2(deltaY, deltaX);
344
-
345
- // Calculate the base points of the tail
346
- const perpAngle = angle + Math.PI / 2;
347
- const basePoint1 = {
348
- x: bubbleCenterX + Math.cos(perpAngle) * tailBaseWidth / 2,
349
- y: bubbleCenterY + Math.sin(perpAngle) * tailBaseWidth / 2
350
- };
351
- const basePoint2 = {
352
- x: bubbleCenterX - Math.cos(perpAngle) * tailBaseWidth / 2,
353
- y: bubbleCenterY - Math.sin(perpAngle) * tailBaseWidth / 2
354
- };
355
-
356
- // Calculate control points for the BΓ©zier curves
357
- const controlPointDistance = tailLength * 0.3;
358
- const controlPoint1 = {
359
- x: basePoint1.x + Math.cos(angle) * controlPointDistance,
360
- y: basePoint1.y + Math.sin(angle) * controlPointDistance
361
- };
362
- const controlPoint2 = {
363
- x: basePoint2.x + Math.cos(angle) * controlPointDistance,
364
- y: basePoint2.y + Math.sin(angle) * controlPointDistance
365
- };
366
-
367
- // Draw the tail
368
- ctx.beginPath();
369
- ctx.moveTo(basePoint1.x, basePoint1.y);
370
- ctx.quadraticCurveTo(controlPoint1.x, controlPoint1.y, tailEndX, tailEndY);
371
- ctx.quadraticCurveTo(controlPoint2.x, controlPoint2.y, basePoint2.x, basePoint2.y);
372
- ctx.closePath();
373
-
374
- // Fill and stroke the tail
375
- ctx.fillStyle = 'white';
376
- ctx.fill();
377
- ctx.strokeStyle = 'black';
378
- ctx.stroke();
379
- }
380
-
381
- function adjustBubbleLocation(
382
- location: { x: number; y: number },
383
- width: number,
384
- height: number,
385
- characterBoundingBox: BoundingBox | null,
386
- imageWidth: number,
387
- imageHeight: number,
388
- borderPadding: number
389
- ): { x: number; y: number } {
390
- let adjustedX = location.x;
391
- let adjustedY = location.y;
392
-
393
- // Ensure the bubble doesn't overlap with the character
394
- if (characterBoundingBox) {
395
- const characterMiddle = characterBoundingBox.left + characterBoundingBox.width / 2;
396
- if (Math.abs(adjustedX - characterMiddle) < width / 2) {
397
- // If the bubble is in the middle of the character, move it to the side
398
- adjustedX = adjustedX < characterMiddle
399
- ? Math.max(width / 2 + borderPadding, characterBoundingBox.left - width / 2 - 10)
400
- : Math.min(imageWidth - width / 2 - borderPadding, characterBoundingBox.left + characterBoundingBox.width + width / 2 + 10);
401
- }
402
- }
403
-
404
- // Ensure the bubble (including text) is fully visible
405
- adjustedX = Math.max(width / 2 + borderPadding, Math.min(imageWidth - width / 2 - borderPadding, adjustedX));
406
- adjustedY = Math.max(height / 2 + borderPadding, Math.min(imageHeight - height / 2 - borderPadding, adjustedY));
407
-
408
- return { x: adjustedX, y: adjustedY };
409
- }
410
-
411
- function drawBubbleShape(
412
- ctx: CanvasRenderingContext2D,
413
- shape: "oval" | "rectangular" | "cloud" | "thought",
414
- bubbleLocation: { x: number, y: number },
415
- width: number,
416
- height: number,
417
- tailTarget: { x: number, y: number } | null
418
- ) {
419
- switch (shape) {
420
- case "oval":
421
- drawOvalBubble(ctx, bubbleLocation, width, height);
422
- break;
423
- case "rectangular":
424
- drawRectangularBubble(ctx, bubbleLocation, width, height);
425
- break;
426
- case "cloud":
427
- drawCloudBubble(ctx, bubbleLocation, width, height);
428
- break;
429
- case "thought":
430
- drawThoughtBubble(ctx, bubbleLocation, width, height);
431
- break;
432
- }
433
- }
434
-
435
- function drawOvalBubble(ctx: CanvasRenderingContext2D, location: { x: number, y: number }, width: number, height: number) {
436
- ctx.beginPath();
437
- ctx.ellipse(location.x, location.y, width / 2, height / 2, 0, 0, 2 * Math.PI);
438
- ctx.closePath();
439
- }
440
-
441
- function drawRectangularBubble(ctx: CanvasRenderingContext2D, location: { x: number, y: number }, width: number, height: number) {
442
- const radius = 20;
443
- ctx.beginPath();
444
- ctx.moveTo(location.x - width / 2 + radius, location.y - height / 2);
445
- ctx.lineTo(location.x + width / 2 - radius, location.y - height / 2);
446
- ctx.quadraticCurveTo(location.x + width / 2, location.y - height / 2, location.x + width / 2, location.y - height / 2 + radius);
447
- ctx.lineTo(location.x + width / 2, location.y + height / 2 - radius);
448
- ctx.quadraticCurveTo(location.x + width / 2, location.y + height / 2, location.x + width / 2 - radius, location.y + height / 2);
449
- ctx.lineTo(location.x - width / 2 + radius, location.y + height / 2);
450
- ctx.quadraticCurveTo(location.x - width / 2, location.y + height / 2, location.x - width / 2, location.y + height / 2 - radius);
451
- ctx.lineTo(location.x - width / 2, location.y - height / 2 + radius);
452
- ctx.quadraticCurveTo(location.x - width / 2, location.y - height / 2, location.x - width / 2 + radius, location.y - height / 2);
453
- ctx.closePath();
454
- }
455
-
456
- function drawCloudBubble(ctx: CanvasRenderingContext2D, location: { x: number, y: number }, width: number, height: number) {
457
- const numBumps = Math.floor(width / 40);
458
- const bumpRadius = width / (numBumps * 2);
459
-
460
- ctx.beginPath();
461
- ctx.moveTo(location.x - width / 2 + bumpRadius, location.y);
462
-
463
- // Top
464
- for (let i = 0; i < numBumps; i++) {
465
- const x = location.x - width / 2 + (i * 2 + 1) * bumpRadius;
466
- const y = location.y - height / 2;
467
- ctx.quadraticCurveTo(x, y - bumpRadius / 2, x + bumpRadius, y);
468
- }
469
-
470
- // Right
471
- for (let i = 0; i < numBumps / 2; i++) {
472
- const x = location.x + width / 2;
473
- const y = location.y - height / 2 + (i * 2 + 1) * bumpRadius * 2;
474
- ctx.quadraticCurveTo(x + bumpRadius / 2, y, x, y + bumpRadius * 2);
475
- }
476
-
477
- // Bottom
478
- for (let i = numBumps; i > 0; i--) {
479
- const x = location.x - width / 2 + (i * 2 - 1) * bumpRadius;
480
- const y = location.y + height / 2;
481
- ctx.quadraticCurveTo(x, y + bumpRadius / 2, x - bumpRadius, y);
482
- }
483
-
484
- // Left
485
- for (let i = numBumps / 2; i > 0; i--) {
486
- const x = location.x - width / 2;
487
- const y = location.y - height / 2 + (i * 2 - 1) * bumpRadius * 2;
488
- ctx.quadraticCurveTo(x - bumpRadius / 2, y, x, y - bumpRadius * 2);
489
- }
490
- ctx.closePath();
491
- }
492
-
493
- function drawThoughtBubble(ctx: CanvasRenderingContext2D, location: { x: number, y: number }, width: number, height: number) {
494
- drawCloudBubble(ctx, location, width, height);
495
- // The tail for thought bubbles is handled in the drawTail function
496
- }
497
-
498
- function wrapText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number, lineHeight: number): string[] {
499
- const words = text.split(' ');
500
- const lines: string[] = [];
501
- let currentLine = '';
502
-
503
- for (const word of words) {
504
- const testLine = currentLine + (currentLine ? ' ' : '') + word;
505
- const metrics = ctx.measureText(testLine);
506
-
507
- if (metrics.width > maxWidth) {
508
- lines.push(currentLine);
509
- currentLine = word;
510
- } else {
511
- currentLine = testLine;
512
- }
513
- }
514
-
515
- if (currentLine) {
516
- lines.push(currentLine);
517
- }
518
-
519
- return lines;
520
- }
521
-
522
- function measureTextDimensions(ctx: CanvasRenderingContext2D, lines: string[], lineHeight: number): { width: number, height: number } {
523
- let maxWidth = 0;
524
- const height = lineHeight * lines.length;
525
-
526
- for (const line of lines) {
527
- const metrics = ctx.measureText(line);
528
- maxWidth = Math.max(maxWidth, metrics.width);
529
- }
530
-
531
- return { width: maxWidth, height };
532
- }
533
-
534
- function drawFormattedText(ctx: CanvasRenderingContext2D, lines: string[], x: number, y: number, maxWidth: number, lineHeight: number) {
535
- const totalHeight = lineHeight * lines.length;
536
- let startY = y - totalHeight / 2 + lineHeight / 2;
537
-
538
- for (let i = 0; i < lines.length; i++) {
539
- const line = lines[i];
540
- const lineY = startY + i * lineHeight;
541
- ctx.fillText(line, x, lineY, maxWidth);
542
- }
543
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/createLlamaPrompt.ts CHANGED
@@ -3,7 +3,7 @@ export function createLlamaPrompt(messages: Array<{ role: string, content: strin
3
  const B_INST = "[INST]", E_INST = "[/INST]";
4
  const B_SYS = "<<SYS>>\n", E_SYS = "\n<</SYS>>\n\n";
5
  const BOS = "<s>", EOS = "</s>";
6
- const DEFAULT_SYSTEM_PROMPT = "You are a helpful, respectful and honest storywriting assistant. Always answer in a creative and entertaining way, while being safe. Please ensure that your stories, speeches and captions are socially unbiased and positive in nature. If a request does not make any sense, go on anyway, as we are writing a fantasy story.";
7
 
8
  if (messages[0].role != "system"){
9
  messages = [
 
3
  const B_INST = "[INST]", E_INST = "[/INST]";
4
  const B_SYS = "<<SYS>>\n", E_SYS = "\n<</SYS>>\n\n";
5
  const BOS = "<s>", EOS = "</s>";
6
+ const DEFAULT_SYSTEM_PROMPT = "You are a helpful, respectful and honest storywriting assistant. Always answer in a creative and entertaining way, while being safe. Please ensure that your stories and captions are socially unbiased and positive in nature. If a request does not make any sense, go on anyway, as we are writing a fantasy story.";
7
 
8
  if (messages[0].role != "system"){
9
  messages = [
src/lib/dirtyGeneratedPanelCleaner.ts CHANGED
@@ -3,10 +3,8 @@ import { GeneratedPanel } from "@/types"
3
  export function dirtyGeneratedPanelCleaner({
4
  panel,
5
  instructions,
6
- speech,
7
  caption
8
  }: GeneratedPanel): GeneratedPanel {
9
- let newSpeech = `${speech || ""}`.split(":").pop()?.trim() || ""
10
  let newCaption = `${caption || ""}`.split(":").pop()?.trim() || ""
11
  let newInstructions = (
12
  // need to remove from LLM garbage here, too
@@ -36,7 +34,6 @@ export function dirtyGeneratedPanelCleaner({
36
  return {
37
  panel,
38
  instructions: newInstructions,
39
- speech: newSpeech,
40
  caption: newCaption,
41
  }
42
  }
 
3
  export function dirtyGeneratedPanelCleaner({
4
  panel,
5
  instructions,
 
6
  caption
7
  }: GeneratedPanel): GeneratedPanel {
 
8
  let newCaption = `${caption || ""}`.split(":").pop()?.trim() || ""
9
  let newInstructions = (
10
  // need to remove from LLM garbage here, too
 
34
  return {
35
  panel,
36
  instructions: newInstructions,
 
37
  caption: newCaption,
38
  }
39
  }
src/lib/dirtyGeneratedPanelsParser.ts CHANGED
@@ -14,18 +14,15 @@ export function dirtyGeneratedPanelsParser(input: string): GeneratedPanel[] {
14
 
15
  const results = jsonData.map((item, i) => {
16
  let panel = i
17
- let speech = item.speech ? item.speech.trim() : ''
18
  let caption = item.caption ? item.caption.trim() : ''
19
  let instructions = item.instructions ? item.instructions.trim() : ''
20
- if (!instructions && !caption && speech) {
21
- instructions = speech
22
- } else if (!instructions && caption) {
23
  instructions = caption
24
  }
25
  if (!caption && instructions) {
26
  caption = instructions
27
  }
28
- return { panel, speech, caption, instructions }
29
  })
30
 
31
  return results
 
14
 
15
  const results = jsonData.map((item, i) => {
16
  let panel = i
 
17
  let caption = item.caption ? item.caption.trim() : ''
18
  let instructions = item.instructions ? item.instructions.trim() : ''
19
+ if (!instructions && caption) {
 
 
20
  instructions = caption
21
  }
22
  if (!caption && instructions) {
23
  caption = instructions
24
  }
25
+ return { panel, caption, instructions }
26
  })
27
 
28
  return results
src/lib/fileToBase64.ts DELETED
@@ -1,8 +0,0 @@
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/getImageDimension.ts CHANGED
@@ -1,26 +1,16 @@
1
- import { ClapImageRatio } from "@aitube/clap"
2
-
3
  export interface ImageDimension {
4
  width: number
5
  height: number
6
- orientation: ClapImageRatio
7
  }
8
 
9
  export async function getImageDimension(src: string): Promise<ImageDimension> {
10
  if (!src) {
11
- return { width: 0, height: 0, orientation: ClapImageRatio.SQUARE }
12
  }
13
  const img = new Image()
14
  img.src = src
15
  await img.decode()
16
  const width = img.width
17
  const height = img.height
18
-
19
- let orientation = ClapImageRatio.SQUARE
20
- if (width > height) {
21
- orientation = ClapImageRatio.LANDSCAPE
22
- } else if (width < height) {
23
- orientation = ClapImageRatio.PORTRAIT
24
- }
25
- return { width, height, orientation }
26
  }
 
 
 
1
  export interface ImageDimension {
2
  width: number
3
  height: number
 
4
  }
5
 
6
  export async function getImageDimension(src: string): Promise<ImageDimension> {
7
  if (!src) {
8
+ return { width: 0, height: 0 }
9
  }
10
  const img = new Image()
11
  img.src = src
12
  await img.decode()
13
  const width = img.width
14
  const height = img.height
15
+ return { width, height }
 
 
 
 
 
 
 
16
  }
src/lib/getLocalStorageShowSpeeches.ts DELETED
@@ -1,13 +0,0 @@
1
- export function getLocalStorageShowSpeeches(defaultValue: boolean): boolean {
2
- try {
3
- const result = localStorage.getItem("AI_COMIC_FACTORY_SHOW_SPEECHES")
4
- if (typeof result !== "string") {
5
- return defaultValue
6
- }
7
- if (result === "true") { return true }
8
- if (result === "false") { return false }
9
- return defaultValue
10
- } catch (err) {
11
- return defaultValue
12
- }
13
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/getOAuthRedirectUrl.ts DELETED
@@ -1,11 +0,0 @@
1
- export function getOAuthRedirectUrl(): string {
2
- if (typeof window === "undefined") {
3
- return "http://localhost:3000"
4
- }
5
-
6
- return (
7
- window.location.hostname === "aicomicfactory.app" ? "https://aicomicfactory.app"
8
- : window.location.hostname === "jbilcke-hf-ai-comic-factory.hf.space" ? "https://jbilcke-hf-ai-comic-factory.hf.space"
9
- : "http://localhost:3000"
10
- )
11
- }