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 +8 -18
  2. .nvmrc +1 -1
  3. README.md +6 -17
  4. package-lock.json +0 -0
  5. package.json +13 -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 +0 -27
  35. src/app/queries/getUserPrompt.ts +0 -9
  36. src/app/queries/mockLLMResponse.ts +3 -11
  37. src/app/queries/predict.ts +9 -19
  38. src/app/queries/predictNextPanels.ts +34 -41
  39. src/app/queries/predictWithAnthropic.ts +0 -48
  40. src/app/queries/predictWithGroq.ts +4 -21
  41. src/app/queries/predictWithHuggingFace.ts +3 -15
  42. src/app/queries/predictWithOpenAI.ts +7 -26
  43. src/app/store/index.ts +16 -348
  44. src/lib/bubble/injectSpeechBubbleInTheBackground.ts +0 -543
  45. src/lib/createLlamaPrompt.ts +1 -1
  46. src/lib/dirtyGeneratedPanelCleaner.ts +0 -3
  47. src/lib/dirtyGeneratedPanelsParser.ts +2 -5
  48. src/lib/fileToBase64.ts +0 -8
  49. src/lib/getImageDimension.ts +2 -12
  50. src/lib/getLocalStorageShowSpeeches.ts +0 -13
.env CHANGED
@@ -11,7 +11,6 @@ RENDERING_ENGINE="INFERENCE_API"
11
  # - INFERENCE_API
12
  # - OPENAI
13
  # - GROQ
14
- # - ANTHROPIC
15
  LLM_ENGINE="INFERENCE_API"
16
 
17
  # set this to control the number of pages
@@ -24,8 +23,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)
@@ -49,22 +46,19 @@ AUTH_VIDEOCHAIN_API_TOKEN=
49
  # Groq.com key: available for the LLM engine
50
  AUTH_GROQ_API_KEY=
51
 
52
- # Anthropic.com key: available for the LLM engine
53
- AUTH_ANTHROPIC_API_KEY=
54
-
55
  # ------------- RENDERING API CONFIG --------------
56
 
57
- # If you decide to use Replicate for the RENDERING engine
58
  RENDERING_REPLICATE_API_MODEL="stabilityai/sdxl"
59
  RENDERING_REPLICATE_API_MODEL_VERSION="da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf"
60
 
61
- # If you decide to use a private Hugging Face Inference Endpoint for the RENDERING engine
62
  RENDERING_HF_INFERENCE_ENDPOINT_URL="https://XXXXXXXXXX.endpoints.huggingface.cloud"
63
 
64
- # If you decide to use a Hugging Face Inference API model for the RENDERING engine
65
  RENDERING_HF_INFERENCE_API_BASE_MODEL="stabilityai/stable-diffusion-xl-base-1.0"
66
 
67
- # If you decide to use a Hugging Face Inference API model for the RENDERING engine
68
  RENDERING_HF_INFERENCE_API_REFINER_MODEL="stabilityai/stable-diffusion-xl-refiner-1.0"
69
 
70
  # If your model returns a different file type (eg. jpg or webp) change it here
@@ -80,18 +74,14 @@ RENDERING_OPENAI_API_MODEL="dall-e-3"
80
 
81
  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
89
- LLM_ANTHROPIC_API_MODEL="claude-3-opus-20240229"
90
 
91
- # If you decide to use a private Hugging Face Inference Endpoint for the LLM engine
92
  LLM_HF_INFERENCE_ENDPOINT_URL=""
93
 
94
- # If you decide to use a Hugging Face Inference API model for the LLM engine
95
  # LLM_HF_INFERENCE_API_MODEL="HuggingFaceH4/zephyr-7b-beta"
96
  LLM_HF_INFERENCE_API_MODEL="HuggingFaceH4/zephyr-7b-beta"
97
 
 
11
  # - INFERENCE_API
12
  # - OPENAI
13
  # - GROQ
 
14
  LLM_ENGINE="INFERENCE_API"
15
 
16
  # set this to control the number of pages
 
23
  ENABLE_HUGGING_FACE_OAUTH=
24
  ENABLE_HUGGING_FACE_OAUTH_WALL=
25
  HUGGING_FACE_OAUTH_CLIENT_ID=
 
 
26
  HUGGING_FACE_OAUTH_REDIRECT_URL=
27
 
28
  # this one must be kept secret (and is unused for now)
 
46
  # Groq.com key: available for the LLM engine
47
  AUTH_GROQ_API_KEY=
48
 
 
 
 
49
  # ------------- RENDERING API CONFIG --------------
50
 
51
+ # If you decided to use Replicate for the RENDERING engine
52
  RENDERING_REPLICATE_API_MODEL="stabilityai/sdxl"
53
  RENDERING_REPLICATE_API_MODEL_VERSION="da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf"
54
 
55
+ # If you decided to use a private Hugging Face Inference Endpoint for the RENDERING engine
56
  RENDERING_HF_INFERENCE_ENDPOINT_URL="https://XXXXXXXXXX.endpoints.huggingface.cloud"
57
 
58
+ # If you decided to use a Hugging Face Inference API model for the RENDERING engine
59
  RENDERING_HF_INFERENCE_API_BASE_MODEL="stabilityai/stable-diffusion-xl-base-1.0"
60
 
61
+ # If you decided to use a Hugging Face Inference API model for the RENDERING engine
62
  RENDERING_HF_INFERENCE_API_REFINER_MODEL="stabilityai/stable-diffusion-xl-refiner-1.0"
63
 
64
  # If your model returns a different file type (eg. jpg or webp) change it here
 
74
 
75
  LLM_GROQ_API_MODEL="mixtral-8x7b-32768"
76
 
77
+ # If you decided to use OpenAI for the LLM engine
78
  LLM_OPENAI_API_BASE_URL="https://api.openai.com/v1"
79
+ LLM_OPENAI_API_MODEL="gpt-4"
 
 
 
 
80
 
81
+ # If you decided to use a private Hugging Face Inference Endpoint for the LLM engine
82
  LLM_HF_INFERENCE_ENDPOINT_URL=""
83
 
84
+ # If you decided to use a Hugging Face Inference API model for the LLM engine
85
  # LLM_HF_INFERENCE_API_MODEL="HuggingFaceH4/zephyr-7b-beta"
86
  LLM_HF_INFERENCE_API_MODEL="HuggingFaceH4/zephyr-7b-beta"
87
 
.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
@@ -31,14 +31,13 @@ it requires various components to run for the frontend, backend, LLM, SDXL etc.
31
  If you try to duplicate the project, open the `.env` you will see it requires some variables.
32
 
33
  Provider config:
34
- - `LLM_ENGINE`: can be one of `INFERENCE_API`, `INFERENCE_ENDPOINT`, `OPENAI`, `GROQ`, `ANTHROPIC`
35
  - `RENDERING_ENGINE`: can be one of: "INFERENCE_API", "INFERENCE_ENDPOINT", "REPLICATE", "VIDEOCHAIN", "OPENAI" for now, unless you code your custom solution
36
 
37
  Auth config:
38
  - `AUTH_HF_API_TOKEN`: if you decide to use Hugging Face for the LLM engine (inference api model or a custom inference endpoint)
39
  - `AUTH_OPENAI_API_KEY`: to use OpenAI for the LLM engine
40
  - `AUTH_GROQ_API_KEY`: to use Groq for the LLM engine
41
- - `AUTH_ANTHROPIC_API_KEY`: to use Anthropic (Claude) for the LLM engine
42
  - `AUTH_VIDEOCHAIN_API_TOKEN`: secret token to access the VideoChain API server
43
  - `AUTH_REPLICATE_API_TOKEN`: in case you want to use Replicate.com
44
 
@@ -55,9 +54,8 @@ 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
 
62
  In addition, there are some community sharing variables that you can just ignore.
63
  Those variables are not required to run the AI Comic Factory on your own website or computer
@@ -78,7 +76,7 @@ To customise a variable locally, you should create a `.env.local`
78
 
79
  Currently the AI Comic Factory uses [zephyr-7b-beta](https://huggingface.co/HuggingFaceH4/zephyr-7b-beta) through an [Inference Endpoint](https://huggingface.co/docs/inference-endpoints/index).
80
 
81
- You have multiple options:
82
 
83
  ### Option 1: Use an Inference API model
84
 
@@ -123,7 +121,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
  ```
@@ -136,17 +134,8 @@ LLM_GROQ_API_MODEL="mixtral-8x7b-32768"
136
 
137
  AUTH_GROQ_API_KEY="Your own GROQ API Key"
138
  ```
139
- ### Option 5: (new, experimental) use Anthropic (Claude)
140
 
141
- ```bash
142
- LLM_ENGINE="ANTHROPIC"
143
-
144
- LLM_ANTHROPIC_API_MODEL="claude-3-opus-20240229"
145
-
146
- AUTH_ANTHROPIC_API_KEY="Your own ANTHROPIC API Key"
147
- ```
148
-
149
- ### Option 6: Fork and modify the code to use a different LLM system
150
 
151
  Another option could be to disable the LLM completely and replace it with another LLM protocol and/or provider (eg. Claude, Replicate), or a human-generated story instead (by returning mock or static data).
152
 
 
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
 
31
  If you try to duplicate the project, open the `.env` you will see it requires some variables.
32
 
33
  Provider config:
34
+ - `LLM_ENGINE`: can be one of: "INFERENCE_API", "INFERENCE_ENDPOINT", "OPENAI", or "GROQ"
35
  - `RENDERING_ENGINE`: can be one of: "INFERENCE_API", "INFERENCE_ENDPOINT", "REPLICATE", "VIDEOCHAIN", "OPENAI" for now, unless you code your custom solution
36
 
37
  Auth config:
38
  - `AUTH_HF_API_TOKEN`: if you decide to use Hugging Face for the LLM engine (inference api model or a custom inference endpoint)
39
  - `AUTH_OPENAI_API_KEY`: to use OpenAI for the LLM engine
40
  - `AUTH_GROQ_API_KEY`: to use Groq for the LLM engine
 
41
  - `AUTH_VIDEOCHAIN_API_TOKEN`: secret token to access the VideoChain API server
42
  - `AUTH_REPLICATE_API_TOKEN`: in case you want to use Replicate.com
43
 
 
54
  - `LLM_HF_INFERENCE_ENDPOINT_URL`: "<use your own>"
55
  - `LLM_HF_INFERENCE_API_MODEL`: "HuggingFaceH4/zephyr-7b-beta"
56
  - `LLM_OPENAI_API_BASE_URL`: "https://api.openai.com/v1"
57
+ - `LLM_OPENAI_API_MODEL`: "gpt-4"
58
  - `LLM_GROQ_API_MODEL`: "mixtral-8x7b-32768"
 
59
 
60
  In addition, there are some community sharing variables that you can just ignore.
61
  Those variables are not required to run the AI Comic Factory on your own website or computer
 
76
 
77
  Currently the AI Comic Factory uses [zephyr-7b-beta](https://huggingface.co/HuggingFaceH4/zephyr-7b-beta) through an [Inference Endpoint](https://huggingface.co/docs/inference-endpoints/index).
78
 
79
+ You have three options:
80
 
81
  ### Option 1: Use an Inference API model
82
 
 
121
  # default openai api base url is: https://api.openai.com/v1
122
  LLM_OPENAI_API_BASE_URL="A custom OpenAI API Base URL if you have some special privileges"
123
 
124
+ LLM_OPENAI_API_MODEL="gpt-3.5-turbo"
125
 
126
  AUTH_OPENAI_API_KEY="Yourown OpenAI API Key"
127
  ```
 
134
 
135
  AUTH_GROQ_API_KEY="Your own GROQ API Key"
136
  ```
 
137
 
138
+ ### Option 5: Fork and modify the code to use a different LLM system
 
 
 
 
 
 
 
 
139
 
140
  Another option could be to disable the LLM completely and replace it with another LLM protocol and/or provider (eg. Claude, Replicate), or a human-generated story instead (by returning mock or static data).
141
 
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,8 @@
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 +29,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 +43,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
+ "@huggingface/hub": "^0.14.2",
13
+ "@huggingface/inference": "^2.6.1",
 
 
 
14
  "@radix-ui/react-accordion": "^1.1.2",
15
  "@radix-ui/react-avatar": "^1.0.3",
16
  "@radix-ui/react-checkbox": "^1.0.4",
 
29
  "@radix-ui/react-toast": "^1.1.4",
30
  "@radix-ui/react-tooltip": "^1.0.6",
31
  "@types/node": "20.4.2",
32
+ "@types/react": "18.2.15",
33
+ "@types/react-dom": "18.2.7",
34
  "@types/uuid": "^9.0.2",
35
  "autoprefixer": "10.4.18",
36
  "class-variance-authority": "^0.6.1",
 
43
  "eslint-config-next": "13.4.10",
44
  "groq-sdk": "^0.3.1",
45
  "html2canvas": "^1.4.1",
 
46
  "konva": "^9.2.2",
47
  "lucide-react": "^0.260.0",
48
+ "next": "14.1.4",
 
49
  "openai": "^4.29.2",
50
  "pick": "^0.0.1",
51
  "postcss": "8.4.37",
52
+ "react": "18.2.0",
 
53
  "react-circular-progressbar": "^2.1.0",
54
  "react-contenteditable": "^3.3.7",
55
+ "react-dom": "18.2.0",
56
  "react-draggable": "^4.4.6",
 
57
  "react-icons": "^4.11.0",
58
  "react-konva": "^18.2.10",
59
  "react-virtualized-auto-sizer": "^1.0.20",
60
+ "replicate": "^0.29.0",
61
  "sbd": "^1.0.19",
62
+ "sharp": "^0.33.2",
63
  "tailwind-merge": "^2.2.2",
64
  "tailwindcss": "3.4.1",
65
  "tailwindcss-animate": "^1.0.6",
66
  "ts-node": "^10.9.1",
67
+ "typescript": "5.1.6",
68
+ "usehooks-ts": "^2.9.1",
 
69
  "uuid": "^9.0.0",
70
+ "zustand": "^4.4.1"
 
71
  },
72
  "devDependencies": {
73
  "@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 DELETED
@@ -1,27 +0,0 @@
1
- import { Preset } from "../engine/presets"
2
-
3
- export function getSystemPrompt({
4
- preset,
5
- // prompt,
6
- // existingPanelsTemplate,
7
- firstNextOrLast,
8
- maxNbPanels,
9
- nbPanelsToGenerate,
10
- // nbMaxNewTokens,
11
- }: {
12
- preset: Preset
13
- // prompt: string
14
- // existingPanelsTemplate: string
15
- firstNextOrLast: string
16
- maxNbPanels: number
17
- nbPanelsToGenerate: number
18
- // nbMaxNewTokens: number
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
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/queries/getUserPrompt.ts DELETED
@@ -1,9 +0,0 @@
1
- export function getUserPrompt({
2
- prompt,
3
- existingPanelsTemplate,
4
- }: {
5
- prompt: string
6
- existingPanelsTemplate: string
7
- }) {
8
- return `The story is about: ${prompt}.${existingPanelsTemplate}`
9
- }
 
 
 
 
 
 
 
 
 
 
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,13 @@
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
 
8
+ const llmEngine = `${process.env.LLM_ENGINE || ""}` as LLMEngine
 
 
 
 
 
 
 
 
9
 
10
+ export const predict =
11
+ llmEngine === "GROQ" ? predictWithGroq :
12
+ llmEngine === "OPENAI" ? predictWithOpenAI :
13
+ predictWithHuggingFace
 
 
 
 
src/app/queries/predictNextPanels.ts CHANGED
@@ -1,28 +1,25 @@
1
- import { GeneratedPanel, LLMVendorConfig } from "@/types"
 
 
 
2
  import { cleanJson } from "@/lib/cleanJson"
 
3
  import { dirtyGeneratedPanelCleaner } from "@/lib/dirtyGeneratedPanelCleaner"
4
  import { dirtyGeneratedPanelsParser } from "@/lib/dirtyGeneratedPanelsParser"
5
  import { sleep } from "@/lib/sleep"
6
 
7
- import { Preset } from "../engine/presets"
8
- import { predict } from "./predict"
9
- import { getSystemPrompt } from "./getSystemPrompt"
10
- import { getUserPrompt } from "./getUserPrompt"
11
-
12
  export const predictNextPanels = async ({
13
  preset,
14
  prompt = "",
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,9 +28,10 @@ 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 =
38
  existingPanels.length === 0
39
  ? "first"
@@ -41,34 +39,35 @@ export const predictNextPanels = async ({
41
  ? "last"
42
  : "next"
43
 
44
- const systemPrompt = getSystemPrompt({
45
- preset,
46
- firstNextOrLast,
47
- maxNbPanels,
48
- nbPanelsToGenerate,
49
- })
 
 
 
 
 
 
 
 
 
 
50
 
51
- const userPrompt = getUserPrompt({
52
- prompt,
53
- existingPanelsTemplate,
54
- })
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 +78,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 +109,6 @@ export const predictNextPanels = async ({
115
  .map((cap, i) => ({
116
  panel: i,
117
  caption: cap,
118
- speech: cap,
119
  instructions: cap,
120
  }))
121
  )
 
1
+
2
+ import { predict } from "./predict"
3
+ import { Preset } from "../engine/presets"
4
+ import { GeneratedPanel } from "@/types"
5
  import { cleanJson } from "@/lib/cleanJson"
6
+ import { createZephyrPrompt } from "@/lib/createZephyrPrompt"
7
  import { dirtyGeneratedPanelCleaner } from "@/lib/dirtyGeneratedPanelCleaner"
8
  import { dirtyGeneratedPanelsParser } from "@/lib/dirtyGeneratedPanelsParser"
9
  import { sleep } from "@/lib/sleep"
10
 
 
 
 
 
 
11
  export const predictNextPanels = async ({
12
  preset,
13
  prompt = "",
14
  nbPanelsToGenerate,
15
  maxNbPanels,
16
  existingPanels = [],
 
17
  }: {
18
+ preset: Preset;
19
+ prompt: string;
20
+ nbPanelsToGenerate: number;
21
+ maxNbPanels: number;
22
+ existingPanels: GeneratedPanel[];
 
23
  }): Promise<GeneratedPanel[]> => {
24
  // console.log("predictNextPanels: ", { prompt, nbPanelsToGenerate })
25
  // throw new Error("Planned maintenance")
 
28
  // return mockGeneratedPanels
29
 
30
  const existingPanelsTemplate = existingPanels.length
31
+ ? ` 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)}`
32
  : ''
33
 
34
+
35
  const firstNextOrLast =
36
  existingPanels.length === 0
37
  ? "first"
 
39
  ? "last"
40
  : "next"
41
 
42
+ const query = createZephyrPrompt([
43
+ {
44
+ role: "system",
45
+ content: [
46
+ `You are a writer specialized in ${preset.llmPrompt}`,
47
+ `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).`,
48
+ `Give your response as a VALID JSON array like this: \`Array<{ panel: number; instructions: string; caption: string; }>\`.`,
49
+ // `Give your response as Markdown bullet points.`,
50
+ `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!`
51
+ ].filter(item => item).join("\n")
52
+ },
53
+ {
54
+ role: "user",
55
+ content: `The story is about: ${prompt}.${existingPanelsTemplate}`,
56
+ }
57
+ ]) + "\n[{"
58
 
 
 
 
 
59
 
60
  let result = ""
61
 
62
+ // we don't require a lot of token for our task
63
+ // but to be safe, let's count ~130 tokens per panel
64
+ const nbTokensPerPanel = 130
65
 
66
  const nbMaxNewTokens = nbPanelsToGenerate * nbTokensPerPanel
67
 
68
  try {
69
+ // console.log(`calling predict(${query}, ${nbTotalPanels})`)
70
+ result = `${await predict(query, nbMaxNewTokens)}`.trim()
 
 
 
 
 
71
  console.log("LLM result (1st trial):", result)
72
  if (!result.length) {
73
  throw new Error("empty result on 1st trial!")
 
78
  await sleep(2000)
79
 
80
  try {
81
+ result = `${await predict(query + " \n ", nbMaxNewTokens)}`.trim()
 
 
 
 
 
82
  console.log("LLM result (2nd trial):", result)
83
  if (!result.length) {
84
  throw new Error("empty result on 2nd trial!")
 
109
  .map((cap, i) => ({
110
  panel: i,
111
  caption: cap,
 
112
  instructions: cap,
113
  }))
114
  )
src/app/queries/predictWithAnthropic.ts DELETED
@@ -1,48 +0,0 @@
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
-
7
- 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,
27
- })
28
-
29
- const messages: MessageParam[] = [
30
- { role: "user", content: userPrompt },
31
- ]
32
-
33
- try {
34
- const res = await anthropic.messages.create({
35
- messages: messages,
36
- // stream: false,
37
- system: systemPrompt,
38
- model: anthropicApiModel,
39
- // temperature: 0.8,
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 ""
47
- }
48
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/queries/predictWithGroq.ts CHANGED
@@ -1,34 +1,17 @@
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,
27
  })
28
 
29
  const messages: Groq.Chat.Completions.CompletionCreateParams.Message[] = [
30
- { role: "system", content: systemPrompt },
31
- { role: "user", content: userPrompt },
32
  ]
33
 
34
  try {
 
1
  "use server"
2
 
 
3
  import Groq from "groq-sdk"
4
 
5
+ export async function predict(inputs: string, nbMaxNewTokens: number): Promise<string> {
6
+ const groqApiKey = `${process.env.AUTH_GROQ_API_KEY || ""}`
7
+ const groqApiModel = `${process.env.LLM_GROQ_API_MODEL || "mixtral-8x7b-32768"}`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  const groq = new Groq({
10
  apiKey: groqApiKey,
11
  })
12
 
13
  const messages: Groq.Chat.Completions.CompletionCreateParams.Message[] = [
14
+ { role: "assistant", content: "" },
 
15
  ]
16
 
17
  try {
src/app/queries/predictWithHuggingFace.ts CHANGED
@@ -1,16 +1,9 @@
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
@@ -53,12 +46,7 @@ export async function predict({
53
  try {
54
  for await (const output of api.textGenerationStream({
55
  model: llmEngine === "INFERENCE_ENDPOINT" ? undefined : (inferenceModel || undefined),
56
-
57
- inputs: createZephyrPrompt([
58
- { role: "system", content: systemPrompt },
59
- { role: "user", content: userPrompt }
60
- ]) + "\n[{", // <-- important: we force its hand
61
-
62
  parameters: {
63
  do_sample: true,
64
  max_new_tokens: nbMaxNewTokens,
 
1
  "use server"
2
 
3
  import { HfInference, HfInferenceEndpoint } from "@huggingface/inference"
4
+ import { LLMEngine } from "@/types"
 
 
 
 
 
 
 
 
5
 
6
+ export async function predict(inputs: string, nbMaxNewTokens: number): Promise<string> {
7
  const hf = new HfInference(process.env.AUTH_HF_API_TOKEN)
8
 
9
  const llmEngine = `${process.env.LLM_ENGINE || ""}` as LLMEngine
 
46
  try {
47
  for await (const output of api.textGenerationStream({
48
  model: llmEngine === "INFERENCE_ENDPOINT" ? undefined : (inferenceModel || undefined),
49
+ inputs,
 
 
 
 
 
50
  parameters: {
51
  do_sample: true,
52
  max_new_tokens: nbMaxNewTokens,
src/app/queries/predictWithOpenAI.ts CHANGED
@@ -1,39 +1,20 @@
1
  "use server"
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,
32
  })
33
 
34
- const messages: ChatCompletionMessageParam[] = [
35
- { role: "system", content: systemPrompt },
36
- { role: "user", content: userPrompt },
37
  ]
38
 
39
  try {
 
1
  "use server"
2
 
3
+ import type { ChatCompletionMessage } from "openai/resources/chat"
4
  import OpenAI from "openai"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
+ export async function predict(inputs: string, nbMaxNewTokens: number): Promise<string> {
7
+ const openaiApiKey = `${process.env.AUTH_OPENAI_API_KEY || ""}`
8
  const openaiApiBaseUrl = `${process.env.LLM_OPENAI_API_BASE_URL || "https://api.openai.com/v1"}`
9
+ const openaiApiModel = `${process.env.LLM_OPENAI_API_MODEL || "gpt-3.5-turbo"}`
10
+
11
  const openai = new OpenAI({
12
  apiKey: openaiApiKey,
13
  baseURL: openaiApiBaseUrl,
14
  })
15
 
16
+ const messages: ChatCompletionMessage[] = [
17
+ { role: "assistant", content: inputs },
 
18
  ]
19
 
20
  try {
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
- }