Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
pls1
#698
by
wushenjoy
- opened
This view is limited to 50 files because it contains too many changes.
See the raw diff here.
- .env +8 -18
- .nvmrc +1 -1
- README.md +6 -17
- package-lock.json +0 -0
- package.json +13 -22
- src/app/engine/render.ts +11 -15
- src/app/interface/about/index.tsx +5 -4
- src/app/interface/advert/index.tsx +0 -15
- src/app/interface/ai-clip-factory/index.tsx +15 -0
- src/app/interface/auth-wall/index.tsx +5 -14
- src/app/interface/bottom-bar/bottom-bar.tsx +18 -61
- src/app/interface/discord/index.tsx +0 -20
- src/app/interface/grid/index.tsx +1 -1
- src/app/interface/login/login.tsx +1 -1
- src/app/interface/page/index.tsx +3 -5
- src/app/interface/panel/bubble/index.tsx +2 -3
- src/app/interface/panel/index.tsx +20 -67
- src/app/interface/select-global-layout/index.tsx +0 -39
- src/app/interface/select-layout/index.tsx +0 -56
- src/app/interface/settings-dialog/defaultSettings.ts +2 -5
- src/app/interface/settings-dialog/getSettings.ts +1 -4
- src/app/interface/settings-dialog/index.tsx +36 -174
- src/app/interface/settings-dialog/label.tsx +2 -10
- src/app/interface/settings-dialog/localStorageKeys.ts +18 -25
- src/app/interface/settings-dialog/section-title.tsx +0 -20
- src/app/interface/share/index.tsx +5 -5
- src/app/interface/top-menu/index.tsx +66 -53
- src/app/layouts/index.tsx +0 -15
- src/app/layouts/settings.tsx +0 -52
- src/app/main.tsx +6 -29
- src/app/page.tsx +6 -16
- src/app/queries/getDynamicConfig.ts +0 -3
- src/app/queries/getLLMEngineFunction.ts +0 -19
- src/app/queries/getStoryContinuation.ts +1 -6
- src/app/queries/getSystemPrompt.ts +0 -27
- src/app/queries/getUserPrompt.ts +0 -9
- src/app/queries/mockLLMResponse.ts +3 -11
- src/app/queries/predict.ts +9 -19
- src/app/queries/predictNextPanels.ts +34 -41
- src/app/queries/predictWithAnthropic.ts +0 -48
- src/app/queries/predictWithGroq.ts +4 -21
- src/app/queries/predictWithHuggingFace.ts +3 -15
- src/app/queries/predictWithOpenAI.ts +7 -26
- src/app/store/index.ts +16 -348
- src/lib/bubble/injectSpeechBubbleInTheBackground.ts +0 -543
- src/lib/createLlamaPrompt.ts +1 -1
- src/lib/dirtyGeneratedPanelCleaner.ts +0 -3
- src/lib/dirtyGeneratedPanelsParser.ts +2 -5
- src/lib/fileToBase64.ts +0 -8
- src/lib/getImageDimension.ts +2 -12
.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
|
58 |
RENDERING_REPLICATE_API_MODEL="stabilityai/sdxl"
|
59 |
RENDERING_REPLICATE_API_MODEL_VERSION="da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf"
|
60 |
|
61 |
-
# If you
|
62 |
RENDERING_HF_INFERENCE_ENDPOINT_URL="https://XXXXXXXXXX.endpoints.huggingface.cloud"
|
63 |
|
64 |
-
# If you
|
65 |
RENDERING_HF_INFERENCE_API_BASE_MODEL="stabilityai/stable-diffusion-xl-base-1.0"
|
66 |
|
67 |
-
# If you
|
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
|
84 |
LLM_OPENAI_API_BASE_URL="https://api.openai.com/v1"
|
85 |
-
LLM_OPENAI_API_MODEL="gpt-4
|
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
|
92 |
LLM_HF_INFERENCE_ENDPOINT_URL=""
|
93 |
|
94 |
-
# If you
|
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.
|
|
|
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:
|
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
|
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
|
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
|
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-
|
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 |
-
|
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.
|
4 |
"private": true,
|
5 |
"scripts": {
|
6 |
"dev": "next dev",
|
@@ -9,11 +9,8 @@
|
|
9 |
"lint": "next lint"
|
10 |
},
|
11 |
"dependencies": {
|
12 |
-
"@
|
13 |
-
"@
|
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.
|
36 |
-
"@types/react-dom": "18.
|
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.
|
53 |
-
"npm": "^10.7.0",
|
54 |
"openai": "^4.29.2",
|
55 |
"pick": "^0.0.1",
|
56 |
"postcss": "8.4.37",
|
57 |
-
"
|
58 |
-
"react": "18.3.1",
|
59 |
"react-circular-progressbar": "^2.1.0",
|
60 |
"react-contenteditable": "^3.3.7",
|
61 |
-
"react-dom": "18.
|
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.
|
68 |
"sbd": "^1.0.19",
|
69 |
-
"sharp": "^0.33.
|
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": "
|
75 |
-
"
|
76 |
-
"usehooks-ts": "2.9.1",
|
77 |
"uuid": "^9.0.0",
|
78 |
-
"
|
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
|
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 |
-
|
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(
|
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"
|
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.
|
12 |
-
const APP_RELEASE_DATE = `
|
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">
|
31 |
-
<span className="inline md:hidden"
|
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/index.tsx
DELETED
@@ -1,15 +0,0 @@
|
|
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/FacePoke", "_blank")
|
10 |
-
}}>
|
11 |
-
<span className="hidden md:inline">Try my new space</span>
|
12 |
-
<span className="inline md:hidden">...</span>
|
13 |
-
</Button>
|
14 |
-
)
|
15 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/interface/ai-clip-factory/index.tsx
ADDED
@@ -0,0 +1,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-[
|
11 |
-
<div className="grid gap-4 py-4 text-stone-800
|
12 |
<p className="">
|
13 |
-
The AI Comic Factory is a free app
|
14 |
</p>
|
15 |
<p>
|
16 |
-
|
17 |
-
our service is free of charge but we would like you to sign-in 👇
|
18 |
</p>
|
19 |
<p>
|
20 |
<Login />
|
21 |
</p>
|
22 |
-
|
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'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(
|
24 |
-
// const download = useStore(
|
25 |
-
// const pageToImage = useStore(
|
26 |
|
27 |
-
const isGeneratingStory = useStore(
|
28 |
-
const prompt = useStore(
|
29 |
-
const panelGenerationStatus = useStore(
|
30 |
|
31 |
-
const preset = useStore(
|
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
|
39 |
-
|
40 |
-
const
|
41 |
-
const
|
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 |
-
|
136 |
-
|
|
|
|
|
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 ⌛` : `
|
196 |
}</span>
|
197 |
<span className="inline md:hidden">{
|
198 |
-
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} ⌛` : `
|
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(
|
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}
|
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(
|
11 |
-
const layouts = useStore(
|
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(
|
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(
|
18 |
-
const
|
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 |
-
|
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(
|
54 |
-
const preset = useStore(
|
55 |
|
56 |
-
const setGeneratingImages = useStore(
|
57 |
|
58 |
-
const panels = useStore(
|
59 |
const prompt = panels[panelIndex] || ""
|
60 |
|
61 |
-
const setPanelPrompt = useStore(
|
62 |
-
|
63 |
-
const showSpeeches = useStore(s => s.showSpeeches)
|
64 |
|
65 |
-
const
|
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(
|
72 |
|
73 |
-
const zoomLevel = useStore(
|
74 |
|
75 |
-
const addToUpscaleQueue = useStore(
|
76 |
|
77 |
const [_isPending, startTransition] = useTransition()
|
78 |
-
const renderedScenes = useStore(
|
79 |
-
const setRendered = useStore(
|
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 {
|
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
|
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 {
|
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 {
|
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]
|
121 |
<DialogHeader>
|
122 |
-
<DialogDescription className="w-full text-center text-
|
123 |
-
|
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
|
135 |
<Slider
|
136 |
min={1}
|
137 |
max={maxNbPages}
|
@@ -147,11 +115,31 @@ export function SettingsDialog() {
|
|
147 |
/>
|
148 |
</Field>
|
149 |
}
|
150 |
-
<div className=
|
151 |
-
|
152 |
-
|
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 |
-
<
|
335 |
-
|
336 |
-
|
337 |
-
|
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'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 |
-
|
4 |
-
|
5 |
-
export function Label({ className, children }: {
|
6 |
-
className?: string
|
7 |
-
children: ReactNode
|
8 |
-
}) {
|
9 |
return (
|
10 |
-
<label className={
|
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:
|
9 |
-
renderingUseTurbo:
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
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(
|
10 |
-
const prompt = useStore(
|
11 |
-
const panelGenerationStatus = useStore(
|
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 ⌛` : `
|
123 |
}</span>
|
124 |
<span className="inline md:hidden">{
|
125 |
-
remainingImages ? `${allStatus.length - remainingImages}/${allStatus.length} ⌛` : `
|
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 |
-
|
34 |
-
|
|
|
|
|
|
|
|
|
|
|
35 |
|
36 |
-
|
37 |
-
|
38 |
-
const
|
39 |
-
const
|
40 |
-
const
|
41 |
-
|
42 |
-
|
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
|
50 |
-
const
|
51 |
|
52 |
-
const
|
53 |
-
const
|
54 |
|
55 |
-
const
|
56 |
-
const setCurrentNbPages = useStore(s => s.setCurrentNbPages)
|
57 |
|
58 |
-
const
|
|
|
|
|
59 |
|
60 |
-
const isBusy = useIsBusy()
|
61 |
|
62 |
const [lastDraftPromptA, setLastDraftPromptA] = useLocalStorage<string>(
|
63 |
"AI_COMIC_FACTORY_LAST_DRAFT_PROMPT_A",
|
64 |
-
|
65 |
)
|
66 |
|
67 |
const [lastDraftPromptB, setLastDraftPromptB] = useLocalStorage<string>(
|
68 |
"AI_COMIC_FACTORY_LAST_DRAFT_PROMPT_B",
|
69 |
-
|
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 |
-
<
|
167 |
defaultValue={defaultLayout}
|
168 |
-
|
169 |
disabled={isBusy}
|
170 |
-
|
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
|
180 |
-
<span className="inline
|
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">💬 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 {
|
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(
|
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 |
-
|
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
|
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
|
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 |
-
"
|
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 |
-
"
|
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 |
-
"
|
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
|
4 |
-
import {
|
|
|
|
|
5 |
|
6 |
-
|
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 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
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 |
-
|
|
|
|
|
|
|
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
|
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
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
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 ~
|
60 |
-
const nbTokensPerPanel =
|
61 |
|
62 |
const nbMaxNewTokens = nbPanelsToGenerate * nbTokensPerPanel
|
63 |
|
64 |
try {
|
65 |
-
// console.log(`calling predict
|
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 |
-
|
8 |
-
|
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: "
|
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
|
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 {
|
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:
|
35 |
-
{ role: "
|
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
|
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(
|
111 |
|
112 |
-
currentClap: undefined,
|
113 |
currentNbPanelsPerPage: 4,
|
114 |
maxNbPanelsPerPage: 4,
|
115 |
currentNbPages: 1,
|
116 |
-
maxNbPages:
|
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 |
-
|
127 |
-
showCaptions: getParam("showCaptions", false),
|
128 |
|
129 |
// deprecated?
|
130 |
layout: defaultLayout,
|
131 |
|
132 |
layouts: [defaultLayout, defaultLayout, defaultLayout, defaultLayout],
|
133 |
|
134 |
-
zoomLevel:
|
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
|
343 |
-
const { maxNbPages, currentNbPanelsPerPage
|
344 |
|
|
|
345 |
for (let i = 0; i < maxNbPages; i++) {
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
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
|
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 &&
|
21 |
-
instructions = speech
|
22 |
-
} else if (!instructions && caption) {
|
23 |
instructions = caption
|
24 |
}
|
25 |
if (!caption && instructions) {
|
26 |
caption = instructions
|
27 |
}
|
28 |
-
return { panel,
|
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
|
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 |
}
|