SubspaceDev jbilcke-hf HF Staff commited on
Commit
84d81c2
·
0 Parent(s):

Duplicate from jbilcke-hf/ai-comic-factory

Browse files

Co-authored-by: Julian Bilcke <[email protected]>

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +40 -0
  2. .eslintrc.json +3 -0
  3. .gitignore +35 -0
  4. .nvmrc +1 -0
  5. Dockerfile +65 -0
  6. README.md +128 -0
  7. components.json +16 -0
  8. next.config.js +11 -0
  9. package-lock.json +0 -0
  10. package.json +71 -0
  11. postcss.config.js +6 -0
  12. public/bubble.jpg +0 -0
  13. public/favicon.ico +0 -0
  14. public/favicon/Icon/r +0 -0
  15. public/favicon/favicon-114-precomposed.png +0 -0
  16. public/favicon/favicon-120-precomposed.png +0 -0
  17. public/favicon/favicon-144-precomposed.png +0 -0
  18. public/favicon/favicon-152-precomposed.png +0 -0
  19. public/favicon/favicon-180-precomposed.png +0 -0
  20. public/favicon/favicon-192.png +0 -0
  21. public/favicon/favicon-32.png +0 -0
  22. public/favicon/favicon-36.png +0 -0
  23. public/favicon/favicon-48.png +0 -0
  24. public/favicon/favicon-57.png +0 -0
  25. public/favicon/favicon-60.png +0 -0
  26. public/favicon/favicon-72-precomposed.png +0 -0
  27. public/favicon/favicon-72.png +0 -0
  28. public/favicon/favicon-76.png +0 -0
  29. public/favicon/favicon-96.png +0 -0
  30. public/favicon/favicon.ico +0 -0
  31. public/favicon/index.html +133 -0
  32. public/favicon/manifest.json +41 -0
  33. public/icon.png +0 -0
  34. public/layouts/layout0.jpg +0 -0
  35. public/layouts/layout0_hd.jpg +0 -0
  36. public/layouts/layout1.jpg +0 -0
  37. public/layouts/layout1_hd.jpg +0 -0
  38. public/layouts/layout2.jpg +0 -0
  39. public/layouts/layout2_hd.jpg +0 -0
  40. public/layouts/layout3 hd.jpg +0 -0
  41. public/layouts/layout3.jpg +0 -0
  42. public/mask.png +0 -0
  43. public/next.svg +1 -0
  44. public/vercel.svg +1 -0
  45. src/app/engine/caption.ts +54 -0
  46. src/app/engine/censorship.ts +39 -0
  47. src/app/engine/community.ts +135 -0
  48. src/app/engine/forbidden.ts +6 -0
  49. src/app/engine/presets.ts +559 -0
  50. src/app/engine/render.ts +294 -0
.env ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ------------- IMAGE API CONFIG --------------
2
+ # Supported values:
3
+ # - VIDEOCHAIN
4
+ # - REPLICATE
5
+ RENDERING_ENGINE="REPLICATE"
6
+
7
+ VIDEOCHAIN_API_URL="http://localhost:7860"
8
+ VIDEOCHAIN_API_TOKEN=
9
+
10
+ REPLICATE_API_TOKEN=
11
+ REPLICATE_API_MODEL="stabilityai/sdxl"
12
+ REPLICATE_API_MODEL_VERSION="da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf"
13
+
14
+ # ------------- LLM API CONFIG ----------------
15
+ # Supported values:
16
+ # - INFERENCE_ENDPOINT
17
+ # - INFERENCE_API
18
+ LLM_ENGINE="INFERENCE_ENDPOINT"
19
+
20
+ # Hugging Face token (if you choose to use a custom Inference Endpoint or an Inference API model)
21
+ HF_API_TOKEN=
22
+
23
+ # URL to a custom text-generation Inference Endpoint of your choice
24
+ # -> You can leave it empty if you decide to use an Inference API Model instead
25
+ HF_INFERENCE_ENDPOINT_URL=
26
+
27
+ # You can also use a model from the Inference API (not a custom inference endpoint)
28
+ # -> You can leave it empty if you decide to use an Inference Endpoint URL instead
29
+ HF_INFERENCE_API_MODEL="codellama/CodeLlama-7b-hf"
30
+
31
+ # Not supported yet
32
+ OPENAI_TOKEN=
33
+
34
+ # ----------- COMMUNITY SHARING (OPTIONAL) -----------
35
+ NEXT_PUBLIC_ENABLE_COMMUNITY_SHARING="false"
36
+ # You don't need those community sharing options to run the AI Comic Factory
37
+ # locally or on your own server (they are meant to be used by the Hugging Face team)
38
+ COMMUNITY_API_URL=
39
+ COMMUNITY_API_TOKEN=
40
+ COMMUNITY_API_ID=
.eslintrc.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "extends": "next/core-web-vitals"
3
+ }
.gitignore ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+
8
+ # testing
9
+ /coverage
10
+
11
+ # next.js
12
+ /.next/
13
+ /out/
14
+
15
+ # production
16
+ /build
17
+
18
+ # misc
19
+ .DS_Store
20
+ *.pem
21
+
22
+ # debug
23
+ npm-debug.log*
24
+ yarn-debug.log*
25
+ yarn-error.log*
26
+
27
+ # local env files
28
+ .env*.local
29
+
30
+ # vercel
31
+ .vercel
32
+
33
+ # typescript
34
+ *.tsbuildinfo
35
+ next-env.d.ts
.nvmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ v18.16.0
Dockerfile ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-alpine AS base
2
+
3
+ # Install dependencies only when needed
4
+ FROM base AS deps
5
+ # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
6
+ RUN apk add --no-cache libc6-compat
7
+ WORKDIR /app
8
+
9
+ # Install dependencies based on the preferred package manager
10
+ COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
11
+ RUN \
12
+ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
13
+ elif [ -f package-lock.json ]; then npm ci; \
14
+ elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
15
+ else echo "Lockfile not found." && exit 1; \
16
+ fi
17
+
18
+ # Uncomment the following lines if you want to use a secret at buildtime,
19
+ # for example to access your private npm packages
20
+ # RUN --mount=type=secret,id=HF_EXAMPLE_SECRET,mode=0444,required=true \
21
+ # $(cat /run/secrets/HF_EXAMPLE_SECRET)
22
+
23
+ # Rebuild the source code only when needed
24
+ FROM base AS builder
25
+ WORKDIR /app
26
+ COPY --from=deps /app/node_modules ./node_modules
27
+ COPY . .
28
+
29
+ # Next.js collects completely anonymous telemetry data about general usage.
30
+ # Learn more here: https://nextjs.org/telemetry
31
+ # Uncomment the following line in case you want to disable telemetry during the build.
32
+ # ENV NEXT_TELEMETRY_DISABLED 1
33
+
34
+ # RUN yarn build
35
+
36
+ # If you use yarn, comment out this line and use the line above
37
+ RUN npm run build
38
+
39
+ # Production image, copy all the files and run next
40
+ FROM base AS runner
41
+ WORKDIR /app
42
+
43
+ ENV NODE_ENV production
44
+ # Uncomment the following line in case you want to disable telemetry during runtime.
45
+ # ENV NEXT_TELEMETRY_DISABLED 1
46
+
47
+ RUN addgroup --system --gid 1001 nodejs
48
+ RUN adduser --system --uid 1001 nextjs
49
+
50
+ COPY --from=builder /app/public ./public
51
+
52
+ # Automatically leverage output traces to reduce image size
53
+ # https://nextjs.org/docs/advanced-features/output-file-tracing
54
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
55
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
56
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/cache ./.next/cache
57
+ # COPY --from=builder --chown=nextjs:nodejs /app/.next/cache/fetch-cache ./.next/cache/fetch-cache
58
+
59
+ USER nextjs
60
+
61
+ EXPOSE 3000
62
+
63
+ ENV PORT 3000
64
+
65
+ CMD ["node", "server.js"]
README.md ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: AI Comic Factory
3
+ emoji: 👩‍🎨
4
+ colorFrom: red
5
+ colorTo: yellow
6
+ sdk: docker
7
+ app_port: 3000
8
+ duplicated_from: jbilcke-hf/ai-comic-factory
9
+ ---
10
+
11
+ # AI Comic Factory
12
+
13
+ ## Running the project at home
14
+
15
+ First, I would like to highlight that everything is open-source (see [here](https://huggingface.co/spaces/jbilcke-hf/ai-comic-factory/tree/main), [here](https://huggingface.co/spaces/jbilcke-hf/VideoChain-API/tree/main), [here](https://huggingface.co/spaces/hysts/SD-XL/tree/main), [here](https://github.com/huggingface/text-generation-inference)).
16
+
17
+ However the project isn't a monolithic Space that can be duplicated and ran immediately:
18
+ it requires various components to run for the frontend, backend, LLM, SDXL etc.
19
+
20
+ If you try to duplicate the project and open the `.env` you will see it requires some variables:
21
+
22
+ - `LLM_ENGINE`: can be either "INFERENCE_API" or "INFERENCE_ENDPOINT"
23
+ - `HF_API_TOKEN`: necessary if you decide to use an inference api model or a custom inference endpoint
24
+ - `HF_INFERENCE_ENDPOINT_URL`: necessary if you decide to use a custom inference endpoint
25
+ - `RENDERING_ENGINE`: can only be "VIDEOCHAIN" or "REPLICATE" for now, unless you code your custom solution
26
+ - `VIDEOCHAIN_API_URL`: url to the VideoChain API server
27
+ - `VIDEOCHAIN_API_TOKEN`: secret token to access the VideoChain API server
28
+ - `REPLICATE_API_TOKEN`: in case you want to use Replicate.com
29
+ - `REPLICATE_API_MODEL`: optional, defaults to "stabilityai/sdxl"
30
+ - `REPLICATE_API_MODEL_VERSION`: optional, in case you want to change the version
31
+
32
+ In addition, there are some community sharing variables that you can just ignore.
33
+ Those variables are not required to run the AI Comic Factory on your own website or computer
34
+ (they are meant to create a connection with the Hugging Face community,
35
+ and thus only make sense for official Hugging Face apps):
36
+ - `NEXT_PUBLIC_ENABLE_COMMUNITY_SHARING`: you don't need this
37
+ - `COMMUNITY_API_URL`: you don't need this
38
+ - `COMMUNITY_API_TOKEN`: you don't need this
39
+ - `COMMUNITY_API_ID`: you don't need this
40
+
41
+ Please read the `.env` default config file for more informations.
42
+ To customise a variable locally, you should create a `.env.local`
43
+ (do not commit this file as it will contain your secrets).
44
+
45
+ -> If you intend to run it with local, cloud-hosted and/or proprietary models **you are going to need to code 👨‍💻**.
46
+
47
+ ## The LLM API (Large Language Model)
48
+
49
+ Currently the AI Comic Factory uses [Llama-2 70b](https://huggingface.co/blog/llama2) through an [Inference Endpoint](https://huggingface.co/docs/inference-endpoints/index).
50
+
51
+ You have three options:
52
+
53
+ ### Option 1: Use an Inference API model
54
+
55
+ This is a new option added recently, where you can use one of the models from the Hugging Face Hub. By default we suggest to use CodeLlama 34b as it will provide better results than the 7b model.
56
+
57
+ To activate it, create a `.env.local` configuration file:
58
+
59
+ ```bash
60
+ LLM_ENGINE="INFERENCE_API"
61
+
62
+ HF_API_TOKEN="Your Hugging Face token"
63
+
64
+ # codellama/CodeLlama-7b-hf" is used by default, but you can change this
65
+ # note: You should use a model able to generate JSON responses,
66
+ # so it is storngly suggested to use at least the 34b model
67
+ HF_INFERENCE_API_MODEL="codellama/CodeLlama-7b-hf"
68
+ ```
69
+
70
+ ### Option 2: Use an Inference Endpoint URL
71
+
72
+ If you would like to run the AI Comic Factory on a private LLM running on the Hugging Face Inference Endpoint service, create a `.env.local` configuration file:
73
+
74
+ ```bash
75
+ LLM_ENGINE="INFERENCE_ENDPOINT"
76
+
77
+ HF_API_TOKEN="Your Hugging Face token"
78
+
79
+ HF_INFERENCE_ENDPOINT_URL="path to your inference endpoint url"
80
+ ```
81
+
82
+ To run this kind of LLM locally, you can use [TGI](https://github.com/huggingface/text-generation-inference) (Please read [this post](https://github.com/huggingface/text-generation-inference/issues/726) for more information about the licensing).
83
+
84
+ ### Option 3: Fork and modify the code to use a different LLM system
85
+
86
+ Another option could be to disable the LLM completely and replace it with another LLM protocol and/or provider (eg. OpenAI, Replicate), or a human-generated story instead (by returning mock or static data).
87
+
88
+
89
+ ### Notes
90
+
91
+ It is possible that I modify the AI Comic Factory to make it easier in the future (eg. add support for OpenAI or Replicate)
92
+
93
+ ## The Rendering API
94
+
95
+ This API is used to generate the panel images. This is an API I created for my various projects at Hugging Face.
96
+
97
+ I haven't written documentation for it yet, but basically it is "just a wrapper ™" around other existing APIs:
98
+
99
+ - The [hysts/SD-XL](https://huggingface.co/spaces/hysts/SD-XL?duplicate=true) Space by [@hysts](https://huggingface.co/hysts)
100
+ - And other APIs for making videos, adding audio etc.. but you won't need them for the AI Comic Factory
101
+
102
+ ### Option 1: Deploy VideoChain yourself
103
+
104
+ You will have to [clone](https://huggingface.co/spaces/jbilcke-hf/VideoChain-API?duplicate=true) the [source-code](https://huggingface.co/spaces/jbilcke-hf/VideoChain-API/tree/main)
105
+
106
+ Unfortunately, I haven't had the time to write the documentation for VideoChain yet.
107
+ (When I do I will update this document to point to the VideoChain's README)
108
+
109
+
110
+ ### Option 2: Use Replicate
111
+
112
+ To use Replicate, create a `.env.local` configuration file:
113
+
114
+ ```bash
115
+ RENDERING_ENGINE="REPLICATE"
116
+
117
+ REPLICATE_API_TOKEN="Your Replicate token"
118
+
119
+ REPLICATE_API_MODEL="stabilityai/sdxl"
120
+
121
+ REPLICATE_API_MODEL_VERSION="da77bc59ee60423279fd632efb4795ab731d9e3ca9705ef3341091fb989b7eaf"
122
+ ```
123
+
124
+ ### Option 3: Use another SDXL API
125
+
126
+ If you fork the project you will be able to modify the code to use the Stable Diffusion technology of your choice (local, open-source, proprietary, your custom HF Space etc).
127
+
128
+ It would even be something else, such as Dall-E.
components.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "default",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.js",
8
+ "css": "app/globals.css",
9
+ "baseColor": "stone",
10
+ "cssVariables": false
11
+ },
12
+ "aliases": {
13
+ "components": "@/components",
14
+ "utils": "@/lib/utils"
15
+ }
16
+ }
next.config.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ output: 'standalone',
4
+
5
+ experimental: {
6
+ serverActions: true,
7
+ serverActionsBodySizeLimit: '8mb',
8
+ },
9
+ }
10
+
11
+ module.exports = nextConfig
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "@jbilcke/comic-factory",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@huggingface/inference": "^2.6.1",
13
+ "@radix-ui/react-accordion": "^1.1.2",
14
+ "@radix-ui/react-avatar": "^1.0.3",
15
+ "@radix-ui/react-checkbox": "^1.0.4",
16
+ "@radix-ui/react-collapsible": "^1.0.3",
17
+ "@radix-ui/react-dialog": "^1.0.4",
18
+ "@radix-ui/react-dropdown-menu": "^2.0.5",
19
+ "@radix-ui/react-icons": "^1.3.0",
20
+ "@radix-ui/react-label": "^2.0.2",
21
+ "@radix-ui/react-menubar": "^1.0.3",
22
+ "@radix-ui/react-popover": "^1.0.6",
23
+ "@radix-ui/react-select": "^1.2.2",
24
+ "@radix-ui/react-separator": "^1.0.3",
25
+ "@radix-ui/react-slider": "^1.1.2",
26
+ "@radix-ui/react-slot": "^1.0.2",
27
+ "@radix-ui/react-switch": "^1.0.3",
28
+ "@radix-ui/react-toast": "^1.1.4",
29
+ "@radix-ui/react-tooltip": "^1.0.6",
30
+ "@react-pdf/renderer": "^3.1.12",
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.14",
36
+ "class-variance-authority": "^0.6.1",
37
+ "clsx": "^2.0.0",
38
+ "cmdk": "^0.2.0",
39
+ "cookies-next": "^2.1.2",
40
+ "date-fns": "^2.30.0",
41
+ "eslint": "8.45.0",
42
+ "eslint-config-next": "13.4.10",
43
+ "html2canvas": "^1.4.1",
44
+ "lucide-react": "^0.260.0",
45
+ "next": "13.4.10",
46
+ "pick": "^0.0.1",
47
+ "postcss": "8.4.26",
48
+ "react": "18.2.0",
49
+ "react-circular-progressbar": "^2.1.0",
50
+ "react-dom": "18.2.0",
51
+ "react-virtualized-auto-sizer": "^1.0.20",
52
+ "replicate": "^0.17.0",
53
+ "sbd": "^1.0.19",
54
+ "sharp": "^0.32.5",
55
+ "styled-components": "^6.0.7",
56
+ "tailwind-merge": "^1.13.2",
57
+ "tailwindcss": "3.3.3",
58
+ "tailwindcss-animate": "^1.0.6",
59
+ "tesseract.js": "^4.1.2",
60
+ "ts-node": "^10.9.1",
61
+ "typescript": "5.1.6",
62
+ "usehooks-ts": "^2.9.1",
63
+ "uuid": "^9.0.0",
64
+ "zustand": "^4.4.1"
65
+ },
66
+ "devDependencies": {
67
+ "@types/qs": "^6.9.7",
68
+ "@types/react-virtualized": "^9.21.22",
69
+ "@types/sbd": "^1.0.3"
70
+ }
71
+ }
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
public/bubble.jpg ADDED
public/favicon.ico ADDED
public/favicon/Icon/r ADDED
File without changes
public/favicon/favicon-114-precomposed.png ADDED
public/favicon/favicon-120-precomposed.png ADDED
public/favicon/favicon-144-precomposed.png ADDED
public/favicon/favicon-152-precomposed.png ADDED
public/favicon/favicon-180-precomposed.png ADDED
public/favicon/favicon-192.png ADDED
public/favicon/favicon-32.png ADDED
public/favicon/favicon-36.png ADDED
public/favicon/favicon-48.png ADDED
public/favicon/favicon-57.png ADDED
public/favicon/favicon-60.png ADDED
public/favicon/favicon-72-precomposed.png ADDED
public/favicon/favicon-72.png ADDED
public/favicon/favicon-76.png ADDED
public/favicon/favicon-96.png ADDED
public/favicon/favicon.ico ADDED
public/favicon/index.html ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <head>
3
+ <title>
4
+ Favicons
5
+ </title>
6
+ <meta charset="utf-8" />
7
+
8
+ <!-- For old IEs -->
9
+ <link rel="shortcut icon" href="favicon.ico" />
10
+
11
+ <!-- For new browsers multisize ico -->
12
+ <link rel="icon" type="image/x-icon" sizes="16x16 32x32" href="favicon.ico">
13
+
14
+ <!-- Chrome for Android -->
15
+ <link rel="icon" sizes="192x192" href="favicon-192.png">
16
+
17
+ <!-- For iPhone 6+ downscaled for other devices -->
18
+ <link rel="apple-touch-icon" sizes="180x180" href="favicon-180-precomposed.png">
19
+
20
+ <!-- For IE10 Metro -->
21
+ <meta name="msapplication-TileColor" content="#FFFFFF">
22
+ <meta name="msapplication-TileImage" content="favicon-114-precomposed.png">
23
+
24
+ <style>
25
+
26
+ body {
27
+ background-color: #f5f5f5;
28
+ border: 0px;
29
+ margin: 0px;
30
+ padding: 0px;
31
+ font-family: Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,serif;
32
+ color: black;
33
+ }
34
+
35
+ pre {
36
+ margin: 0px;
37
+ color: black;
38
+ padding: 0px 5%;
39
+ }
40
+
41
+ code {
42
+
43
+ }
44
+
45
+ .container {
46
+ background-color: white;
47
+ max-width: 800px;
48
+ width: 100%;
49
+ margin: 0 auto;
50
+ padding: 1% 0;
51
+ height: 100%;
52
+ }
53
+
54
+ .comment {
55
+ color: gray;
56
+ padding: 0px;
57
+ margin: 0px;
58
+ }
59
+
60
+ hr {
61
+ width: 80%;
62
+ padding: 0 5%;
63
+ border-color: #f5f5f5;
64
+ background-color: #D1D1D1;
65
+ }
66
+
67
+ p {
68
+ padding: 1% 5%;
69
+ }
70
+
71
+ </style>
72
+
73
+ </head>
74
+ <body class="">
75
+
76
+ <div class="container">
77
+ <p>
78
+ To use the favicons insert into your head section some of these tags accordly to your needs.
79
+ </p>
80
+ <hr>
81
+ <pre>
82
+ <code>
83
+ <span class="comment">&lt;!-- For old IEs --&gt;</span>
84
+ &lt;link rel=&quot;shortcut icon&quot; href=&quot;favicon.ico&quot; /&gt;
85
+
86
+ <span class="comment">&lt;!-- For new browsers - multisize ico --&gt;</span>
87
+ &lt;link rel=&quot;icon&quot; type=&quot;image/x-icon&quot; sizes=&quot;16x16 32x32&quot; href=&quot;favicon.ico&quot;&gt;
88
+
89
+ <span class="comment">&lt;!-- For iPad with high-resolution Retina display running iOS &ge; 7: --&gt;</span>
90
+ &lt;link rel=&quot;apple-touch-icon&quot; sizes=&quot;152x152&quot; href=&quot;favicon-152-precomposed.png&quot;&gt;
91
+
92
+ <span class="comment">&lt;!-- For iPad with high-resolution Retina display running iOS &le; 6: --&gt;</span>
93
+ &lt;link rel=&quot;apple-touch-icon&quot; sizes=&quot;144x144&quot; href=&quot;favicon-144-precomposed.png&quot;&gt;
94
+
95
+ <span class="comment">&lt;!-- For iPhone with high-resolution Retina display running iOS &ge; 7: --&gt;</span>
96
+ &lt;link rel=&quot;apple-touch-icon&quot; sizes=&quot;120x120&quot; href=&quot;favicon-120-precomposed.png&quot;&gt;
97
+
98
+ <span class="comment">&lt;!-- For iPhone with high-resolution Retina display running iOS &le; 6: --&gt;</span>
99
+ &lt;link rel=&quot;apple-touch-icon&quot; sizes=&quot;114x114&quot; href=&quot;favicon-114-precomposed.png&quot;&gt;
100
+
101
+ <span class="comment">&lt;!-- For iPhone 6+ --&gt;</span>
102
+ &lt;link rel=&quot;apple-touch-icon&quot; sizes=&quot;180x180&quot; href=&quot;favicon-180-precomposed.png&quot;&gt;
103
+
104
+ <span class="comment">&lt;!-- For first- and second-generation iPad: --&gt;</span>
105
+ &lt;link rel=&quot;apple-touch-icon&quot; sizes=&quot;72x72&quot; href=&quot;favicon-72-precomposed.png&quot;&gt;
106
+
107
+ <span class="comment">&lt;!-- For non-Retina iPhone, iPod Touch, and Android 2.1+ devices: --&gt;</span>
108
+ &lt;link rel=&quot;apple-touch-icon&quot; sizes=&quot;57x57&quot; href=&quot;favicon-57.png&quot;&gt;
109
+
110
+ <span class="comment">&lt;!-- For Old Chrome --&gt;</span>
111
+ &lt;link rel=&quot;icon&quot; sizes=&quot;32x32&quot; href=&quot;favicon-32.png&quot; &gt;
112
+
113
+ <span class="comment">&lt;!-- For IE10 Metro --&gt;</span>
114
+ &lt;meta name=&quot;msapplication-TileColor&quot; content=&quot;#FFFFFF&quot;&gt;
115
+ &lt;meta name=&quot;msapplication-TileImage&quot; content=&quot;favicon-144.png&quot;&gt;
116
+ &lt;meta name=&quot;theme-color&quot; content=&quot;#ffffff&quot;&gt;
117
+
118
+ <span class="comment">&lt;!-- Chrome for Android --&gt;</span>
119
+ &lt;link rel=&quot;manifest&quot; href=&quot;manifest.json&quot;&gt;
120
+ &lt;link rel=&quot;icon&quot; sizes=&quot;192x192&quot; href=&quot;favicon-192.png&quot;&gt;
121
+
122
+ </code>
123
+ </pre>
124
+
125
+ <hr>
126
+
127
+ <p>
128
+ For more informations about favicons consult <a href="https://github.com/audreyr/favicon-cheat-sheet">The Favicon Cheat Sheet</a> by Audrey Roy.
129
+ </p>
130
+
131
+ </div>
132
+
133
+ </body>
public/favicon/manifest.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "pollo",
3
+ "icons": [
4
+ {
5
+ "src": "\/favicon-36.png",
6
+ "sizes": "36x36",
7
+ "type": "image\/png",
8
+ "density": 0.75
9
+ },
10
+ {
11
+ "src": "\/favicon-48.png",
12
+ "sizes": "48x48",
13
+ "type": "image\/png",
14
+ "density": 1
15
+ },
16
+ {
17
+ "src": "\/favicon-72.png",
18
+ "sizes": "72x72",
19
+ "type": "image\/png",
20
+ "density": 1.5
21
+ },
22
+ {
23
+ "src": "\/favicon-96.png",
24
+ "sizes": "96x96",
25
+ "type": "image\/png",
26
+ "density": 2
27
+ },
28
+ {
29
+ "src": "\/favicon-144.png",
30
+ "sizes": "144x144",
31
+ "type": "image\/png",
32
+ "density": 3
33
+ },
34
+ {
35
+ "src": "\/favicon-192.png",
36
+ "sizes": "192x192",
37
+ "type": "image\/png",
38
+ "density": 4
39
+ }
40
+ ]
41
+ }
public/icon.png ADDED
public/layouts/layout0.jpg ADDED
public/layouts/layout0_hd.jpg ADDED
public/layouts/layout1.jpg ADDED
public/layouts/layout1_hd.jpg ADDED
public/layouts/layout2.jpg ADDED
public/layouts/layout2_hd.jpg ADDED
public/layouts/layout3 hd.jpg ADDED
public/layouts/layout3.jpg ADDED
public/mask.png ADDED
public/next.svg ADDED
public/vercel.svg ADDED
src/app/engine/caption.ts ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server"
2
+
3
+ import { ImageAnalysisRequest, ImageAnalysisResponse } from "@/types"
4
+
5
+ const apiUrl = `${process.env.VIDEOCHAIN_API_URL || ""}`
6
+
7
+ export async function see({
8
+ prompt,
9
+ imageBase64
10
+ }: {
11
+ prompt: string
12
+ imageBase64: string
13
+ }): Promise<string> {
14
+ if (!prompt) {
15
+ console.error(`cannot call the API without an image, aborting..`)
16
+ throw new Error(`cannot call the API without an image, aborting..`)
17
+ }
18
+
19
+ try {
20
+ const request = {
21
+ prompt,
22
+ image: imageBase64
23
+
24
+ } as ImageAnalysisRequest
25
+
26
+ console.log(`calling ${apiUrl}/analyze called with: `, {
27
+ prompt: request.prompt,
28
+ image: request.image.slice(0, 20)
29
+ })
30
+
31
+ const res = await fetch(`${apiUrl}/analyze`, {
32
+ method: "POST",
33
+ headers: {
34
+ Accept: "application/json",
35
+ "Content-Type": "application/json",
36
+ // Authorization: `Bearer ${process.env.VIDEOCHAIN_API_TOKEN}`,
37
+ },
38
+ body: JSON.stringify(request),
39
+ cache: 'no-store',
40
+ // we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
41
+ // next: { revalidate: 1 }
42
+ })
43
+
44
+ if (res.status !== 200) {
45
+ throw new Error('Failed to fetch data')
46
+ }
47
+
48
+ const response = (await res.json()) as ImageAnalysisResponse
49
+ return response.result
50
+ } catch (err) {
51
+ console.error(err)
52
+ return ""
53
+ }
54
+ }
src/app/engine/censorship.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ // unfortunately due to abuse by some users, I have to add this NSFW filter
4
+ const secretSalt = `${process.env.SECRET_CENSORSHIP_KEY || ""}`
5
+
6
+ // TODO the censorship is not implement yet actually
7
+
8
+ // I don't want to be banned by Replicate because bad actors are asking
9
+ // for some naked anime stuff or whatever
10
+ // I also want to avoid a PR scandal due to some bad user generated content
11
+
12
+ const forbiddenWords = [
13
+ // those keywords have been generated by looking at the logs of the AI Comic Factory
14
+ // those are real requests some users tried to attempt.. :|
15
+ "nazi",
16
+ "hitler",
17
+ "boob",
18
+ "boobs",
19
+ "boobies",
20
+ "nipple",
21
+ "nipples",
22
+ "nude",
23
+ "nudes",
24
+ "naked",
25
+ "pee",
26
+ "peeing",
27
+ "erotic",
28
+ "sexy"
29
+ ]
30
+
31
+ // temporary utility to make sure Replicate doesn't ban my account
32
+ // because of what users do in their prompt
33
+ export const filterOutBadWords = (sentence: string) => {
34
+ const words = sentence.split(" ")
35
+ return words.filter(word => {
36
+ const lowerCase = word.toLocaleLowerCase()
37
+ return !forbiddenWords.includes(lowerCase)
38
+ }).join(" ")
39
+ }
src/app/engine/community.ts ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server"
2
+
3
+ import { v4 as uuidv4 } from "uuid"
4
+
5
+ import { CreatePostResponse, GetAppPostsResponse, Post, PostVisibility } from "@/types"
6
+ import { filterOutBadWords } from "./censorship"
7
+
8
+ const apiUrl = `${process.env.COMMUNITY_API_URL || ""}`
9
+ const apiToken = `${process.env.COMMUNITY_API_TOKEN || ""}`
10
+ const appId = `${process.env.COMMUNITY_API_ID || ""}`
11
+
12
+ export async function postToCommunity({
13
+ prompt,
14
+ assetUrl,
15
+ }: {
16
+ prompt: string
17
+ assetUrl: string
18
+ }): Promise<Post> {
19
+
20
+ prompt = filterOutBadWords(prompt)
21
+
22
+ // if the community API is disabled,
23
+ // we don't fail, we just mock
24
+ if (!apiUrl) {
25
+ const mockPost: Post = {
26
+ postId: uuidv4(),
27
+ appId: "mock",
28
+ prompt,
29
+ previewUrl: assetUrl,
30
+ assetUrl,
31
+ createdAt: new Date().toISOString(),
32
+ visibility: "normal",
33
+ upvotes: 0,
34
+ downvotes: 0
35
+ }
36
+ return mockPost
37
+ }
38
+
39
+ if (!prompt) {
40
+ console.error(`cannot call the community API without a prompt, aborting..`)
41
+ throw new Error(`cannot call the community API without a prompt, aborting..`)
42
+ }
43
+ if (!assetUrl) {
44
+ console.error(`cannot call the community API without an assetUrl, aborting..`)
45
+ throw new Error(`cannot call the community API without an assetUrl, aborting..`)
46
+ }
47
+
48
+ try {
49
+ console.log(`calling POST ${apiUrl}/posts/${appId} with prompt: ${prompt}`)
50
+
51
+ const postId = uuidv4()
52
+
53
+ const post: Partial<Post> = { postId, appId, prompt, assetUrl }
54
+
55
+ console.table(post)
56
+
57
+ const res = await fetch(`${apiUrl}/posts/${appId}`, {
58
+ method: "POST",
59
+ headers: {
60
+ Accept: "application/json",
61
+ "Content-Type": "application/json",
62
+ Authorization: `Bearer ${apiToken}`,
63
+ },
64
+ body: JSON.stringify(post),
65
+ cache: 'no-store',
66
+ // we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
67
+ // next: { revalidate: 1 }
68
+ })
69
+
70
+ // console.log("res:", res)
71
+ // The return value is *not* serialized
72
+ // You can return Date, Map, Set, etc.
73
+
74
+ // Recommendation: handle errors
75
+ if (res.status !== 200) {
76
+ // This will activate the closest `error.js` Error Boundary
77
+ throw new Error('Failed to fetch data')
78
+ }
79
+
80
+ const response = (await res.json()) as CreatePostResponse
81
+ // console.log("response:", response)
82
+ return response.post
83
+ } catch (err) {
84
+ const error = `failed to post to community: ${err}`
85
+ console.error(error)
86
+ throw new Error(error)
87
+ }
88
+ }
89
+
90
+ export async function getLatestPosts(visibility?: PostVisibility): Promise<Post[]> {
91
+
92
+ let posts: Post[] = []
93
+
94
+ // if the community API is disabled we don't fail,
95
+ // we just mock
96
+ if (!apiUrl) {
97
+ return posts
98
+ }
99
+
100
+ try {
101
+ // console.log(`calling GET ${apiUrl}/posts with renderId: ${renderId}`)
102
+ const res = await fetch(`${apiUrl}/posts/${appId}/${
103
+ visibility || "all"
104
+ }`, {
105
+ method: "GET",
106
+ headers: {
107
+ Accept: "application/json",
108
+ "Content-Type": "application/json",
109
+ Authorization: `Bearer ${apiToken}`,
110
+ },
111
+ cache: 'no-store',
112
+ // we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
113
+ // next: { revalidate: 1 }
114
+ })
115
+
116
+ // console.log("res:", res)
117
+ // The return value is *not* serialized
118
+ // You can return Date, Map, Set, etc.
119
+
120
+ // Recommendation: handle errors
121
+ if (res.status !== 200) {
122
+ // This will activate the closest `error.js` Error Boundary
123
+ throw new Error('Failed to fetch data')
124
+ }
125
+
126
+ const response = (await res.json()) as GetAppPostsResponse
127
+ // console.log("response:", response)
128
+ return Array.isArray(response?.posts) ? response?.posts : []
129
+ } catch (err) {
130
+ // const error = `failed to get posts: ${err}`
131
+ // console.error(error)
132
+ // throw new Error(error)
133
+ return []
134
+ }
135
+ }
src/app/engine/forbidden.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+
2
+ // the NSFW has to contain bad words, but doing so might get the code flagged
3
+ // or attract unwanted attention, so we hash them
4
+ export const forbidden = [
5
+ // TODO implement this
6
+ ]
src/app/engine/presets.ts ADDED
@@ -0,0 +1,559 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FontName, actionman, komika, vtc } from "@/lib/fonts"
2
+ import { pick } from "@/lib/pick"
3
+ import { NextFontWithVariable } from "next/dist/compiled/@next/font"
4
+
5
+ export type ComicFamily =
6
+ | "american"
7
+ | "asian"
8
+ | "european"
9
+
10
+ export type ComicColor =
11
+ | "color"
12
+ | "grayscale"
13
+ | "monochrome"
14
+
15
+ export interface Preset {
16
+ id: string
17
+ label: string
18
+ family: ComicFamily
19
+ color: ComicColor
20
+ font: FontName
21
+ llmPrompt: string
22
+ imagePrompt: (prompt: string) => string[]
23
+ negativePrompt: (prompt: string) => string[]
24
+ }
25
+
26
+ // ATTENTION!! negative prompts are not supported by the VideoChain API yet
27
+
28
+ export const presets: Record<string, Preset> = {
29
+ random: {
30
+ id: "random",
31
+ label: "Random style",
32
+ family: "european",
33
+ color: "color",
34
+ font: "actionman",
35
+ llmPrompt: "",
36
+ imagePrompt: (prompt: string) => [],
37
+ negativePrompt: () => [],
38
+ },
39
+ japanese_manga: {
40
+ id: "japanese_manga",
41
+ label: "Japanese",
42
+ family: "asian",
43
+ color: "grayscale",
44
+ font: "actionman",
45
+ llmPrompt: "japanese manga",
46
+ imagePrompt: (prompt: string) => [
47
+ `japanese manga about ${prompt}`,
48
+ "single panel",
49
+ "manga",
50
+ "japanese",
51
+ "grayscale",
52
+ "intricate",
53
+ "detailed",
54
+ // "drawing"
55
+ ],
56
+ negativePrompt: () => [
57
+ "franco-belgian comic",
58
+ "color album",
59
+ "color",
60
+ "american comic",
61
+ "photo",
62
+ "painting",
63
+ "3D render"
64
+ ],
65
+ },
66
+ nihonga: {
67
+ id: "nihonga",
68
+ label: "Nihonga",
69
+ family: "asian",
70
+ color: "color",
71
+ font: "actionman",
72
+ llmPrompt: "japanese manga",
73
+ imagePrompt: (prompt: string) => [
74
+ `japanese nihonga painting about ${prompt}`,
75
+ "Nihonga",
76
+ "ancient japanese painting",
77
+ "intricate",
78
+ "detailed",
79
+ // "drawing"
80
+ ],
81
+ negativePrompt: () => [
82
+ "franco-belgian comic",
83
+ "color album",
84
+ "color",
85
+ "manga",
86
+ "comic",
87
+ "american comic",
88
+ "photo",
89
+ "painting",
90
+ "3D render"
91
+ ],
92
+ },
93
+ franco_belgian: {
94
+ id: "franco_belgian",
95
+ label: "Franco-Belgian",
96
+ family: "european",
97
+ color: "color",
98
+ font: "actionman",
99
+ llmPrompt: "Franco-Belgian comic (a \"bande dessinée\"), in the style of Franquin, Moebius etc",
100
+ imagePrompt: (prompt: string) => [
101
+ `franco-belgian color comic about ${prompt}`,
102
+ "bande dessinée",
103
+ "franco-belgian comic",
104
+ "comic album",
105
+ // "color drawing"
106
+ ],
107
+ negativePrompt: () => [
108
+ "manga",
109
+ "anime",
110
+ "american comic",
111
+ "grayscale",
112
+ "monochrome",
113
+ "photo",
114
+ "painting",
115
+ "3D render"
116
+ ],
117
+ },
118
+ american_comic_90: {
119
+ id: "american_comic_90",
120
+ label: "American (modern)",
121
+ family: "american",
122
+ color: "color",
123
+ font: "actionman",
124
+ llmPrompt: "american comic",
125
+ imagePrompt: (prompt: string) => [
126
+ `modern american comic about ${prompt}`,
127
+ //"single panel",
128
+ "digital color comicbook style",
129
+ // "2010s",
130
+ // "digital print",
131
+ // "color comicbook",
132
+ // "color drawing"
133
+ ],
134
+ negativePrompt: () => [
135
+ "manga",
136
+ "anime",
137
+ "american comic",
138
+ "action",
139
+ "grayscale",
140
+ "monochrome",
141
+ "photo",
142
+ "painting",
143
+ "3D render"
144
+ ],
145
+ },
146
+
147
+ /*
148
+ american_comic_40: {
149
+ label: "American (1940)",
150
+ family: "american",
151
+ color: "color",
152
+ font: "actionman",
153
+ llmPrompt: "american comic",
154
+ imagePrompt: (prompt: string) => [
155
+ `american comic about ${prompt}`,
156
+ "single panel",
157
+ "american comic",
158
+ "comicbook style",
159
+ "1940",
160
+ "40s",
161
+ "color comicbook",
162
+ "color drawing"
163
+ ],
164
+ negativePrompt: () => [
165
+ "manga",
166
+ "anime",
167
+ "american comic",
168
+ "action",
169
+ "grayscale",
170
+ "monochrome",
171
+ "photo",
172
+ "painting",
173
+ "3D render"
174
+ ],
175
+ },
176
+ */
177
+ american_comic_50: {
178
+ id: "american_comic_50",
179
+ label: "American (1950)",
180
+ family: "american",
181
+ color: "color",
182
+ font: "actionman",
183
+ llmPrompt: "american comic",
184
+ imagePrompt: (prompt: string) => [
185
+ `vintage american color comic about ${prompt}`,
186
+ // "single panel",
187
+ // "comicbook style",
188
+ "1950",
189
+ "50s",
190
+ // "color comicbook",
191
+ // "color drawing"
192
+ ],
193
+ negativePrompt: () => [
194
+ "manga",
195
+ "anime",
196
+ "american comic",
197
+ "action",
198
+ "grayscale",
199
+ "monochrome",
200
+ "photo",
201
+ "painting",
202
+ "3D render"
203
+ ],
204
+ },
205
+ /*
206
+ american_comic_60: {
207
+ label: "American (1960)",
208
+ family: "american",
209
+ color: "color",
210
+ font: "actionman",
211
+ llmPrompt: "american comic",
212
+ imagePrompt: (prompt: string) => [
213
+ `american comic about ${prompt}`,
214
+ "single panel",
215
+ "american comic",
216
+ "comicbook style",
217
+ "1960",
218
+ "60s",
219
+ "color comicbook",
220
+ "color drawing"
221
+ ],
222
+ negativePrompt: () => [
223
+ "manga",
224
+ "anime",
225
+ "american comic",
226
+ "action",
227
+ "grayscale",
228
+ "monochrome",
229
+ "photo",
230
+ "painting",
231
+ "3D render"
232
+ ],
233
+ },
234
+ */
235
+
236
+
237
+ flying_saucer: {
238
+ id: "flying_saucer",
239
+ label: "Flying saucer",
240
+ family: "european",
241
+ color: "color",
242
+ font: "actionman",
243
+ llmPrompt: "new pulp science fiction",
244
+ imagePrompt: (prompt: string) => [
245
+ `vintage color pulp comic panel`,
246
+ `${prompt}`,
247
+ "40s",
248
+ "1940",
249
+ "vintage science fiction",
250
+ // "single panel",
251
+ // "comic album"
252
+ ],
253
+ negativePrompt: () => [
254
+ "manga",
255
+ "anime",
256
+ "american comic",
257
+ "grayscale",
258
+ "monochrome",
259
+ "photo",
260
+ "painting",
261
+ "3D render"
262
+ ],
263
+ },
264
+
265
+ humanoid: {
266
+ id: "humanoid",
267
+ label: "Humanoid",
268
+ family: "european",
269
+ color: "color",
270
+ font: "actionman",
271
+ llmPrompt: "new album by moebius",
272
+ imagePrompt: (prompt: string) => [
273
+ `color comic panel`,
274
+ `${prompt}`,
275
+ "style of Moebius",
276
+ "by Moebius",
277
+ "french comic panel",
278
+ "franco-belgian style",
279
+ "bande dessinée",
280
+ "single panel",
281
+ // "comic album"
282
+ ],
283
+ negativePrompt: () => [
284
+ "manga",
285
+ "anime",
286
+ "american comic",
287
+ "grayscale",
288
+ "monochrome",
289
+ "photo",
290
+ "painting",
291
+ "3D render"
292
+ ],
293
+ },
294
+ haddock: {
295
+ id: "haddock",
296
+ label: "Haddock",
297
+ family: "european",
298
+ color: "color",
299
+ font: "actionman",
300
+ llmPrompt: "new album by Hergé",
301
+ imagePrompt: (prompt: string) => [
302
+ `color comic panel`,
303
+ `${prompt}`,
304
+ "style of Hergé",
305
+ "by Hergé",
306
+ "tintin style",
307
+ "french comic panel",
308
+ "franco-belgian style",
309
+ // "color panel",
310
+ // "bande dessinée",
311
+ // "single panel",
312
+ // "comic album"
313
+ ],
314
+ negativePrompt: () => [
315
+ "manga",
316
+ "anime",
317
+ "american comic",
318
+ "grayscale",
319
+ "monochrome",
320
+ "photo",
321
+ "painting",
322
+ "3D render"
323
+ ],
324
+ },
325
+ armorican: {
326
+ id: "armorican",
327
+ label: "Armorican",
328
+ family: "european",
329
+ color: "monochrome",
330
+ font: "actionman",
331
+ llmPrompt: "new color album",
332
+ imagePrompt: (prompt: string) => [
333
+ `color comic panel`,
334
+ `about ${prompt}`,
335
+ "romans",
336
+ "gauls",
337
+ "french comic panel",
338
+ "franco-belgian style",
339
+ "bande dessinée",
340
+ "single panel",
341
+ // "comical",
342
+ // "comic album",
343
+ // "color drawing"
344
+ ],
345
+ negativePrompt: () => [
346
+ "manga",
347
+ "anime",
348
+ "american comic",
349
+ "grayscale",
350
+ "monochrome",
351
+ "photo",
352
+ "painting",
353
+ "3D render"
354
+ ],
355
+ },
356
+ render: {
357
+ id: "render",
358
+ label: "3D Render",
359
+ family: "european",
360
+ color: "color",
361
+ font: "actionman",
362
+ llmPrompt: "new movie",
363
+ imagePrompt: (prompt: string) => [
364
+ `3D render`,
365
+ `Blender`,
366
+ `3D animation`,
367
+ `Unreal engine`,
368
+ `${prompt}`,
369
+ ],
370
+ negativePrompt: () => [
371
+ "manga",
372
+ "anime",
373
+ "american comic",
374
+ "grayscale",
375
+ "monochrome",
376
+ "painting"
377
+ ],
378
+ },
379
+ klimt: {
380
+ id: "klimt",
381
+ label: "Klimt",
382
+ family: "european",
383
+ color: "color",
384
+ font: "actionman",
385
+ llmPrompt: "new story",
386
+ imagePrompt: (prompt: string) => [
387
+ `golden`,
388
+ `patchwork`,
389
+ `style of Gustav Klimt`,
390
+ `Gustav Klimt painting`,
391
+ `${prompt}`,
392
+ ],
393
+ negativePrompt: () => [
394
+ "manga",
395
+ "anime",
396
+ "american comic",
397
+ "grayscale",
398
+ "monochrome",
399
+ "painting"
400
+ ],
401
+ },
402
+ medieval: {
403
+ id: "medieval",
404
+ label: "Medieval",
405
+ family: "european",
406
+ color: "color",
407
+ font: "actionman",
408
+ llmPrompt: "new story",
409
+ imagePrompt: (prompt: string) => [
410
+ `medieval illuminated manuscript`,
411
+ `illuminated manuscript of`,
412
+ // `medieval color engraving`,
413
+ `${prompt}`,
414
+ `medieval`
415
+ ],
416
+ negativePrompt: () => [
417
+ "manga",
418
+ "anime",
419
+ "american comic",
420
+ "grayscale",
421
+ "monochrome",
422
+ "painting"
423
+ ],
424
+ },
425
+ /*
426
+ glass: {
427
+ id: "glass",
428
+ label: "Glass",
429
+ family: "european",
430
+ color: "color",
431
+ font: "actionman",
432
+ llmPrompt: "new movie",
433
+ imagePrompt: (prompt: string) => [
434
+ `stained glass`,
435
+ `vitrail`,
436
+ `stained glass`,
437
+ // `medieval color engraving`,
438
+ `${prompt}`,
439
+ `medieval`,
440
+ ],
441
+ negativePrompt: () => [
442
+ "manga",
443
+ "anime",
444
+ "american comic",
445
+ "grayscale",
446
+ "monochrome",
447
+ "painting"
448
+ ],
449
+ },
450
+ */
451
+ /*
452
+ voynich: {
453
+ id: "voynich",
454
+ label: "Voynich",
455
+ family: "european",
456
+ color: "color",
457
+ font: "actionman",
458
+ llmPrompt: "new movie",
459
+ imagePrompt: (prompt: string) => [
460
+ `voynich`,
461
+ `voynich page`,
462
+ // `medieval color engraving`,
463
+ `${prompt}`,
464
+ `medieval`,
465
+ ],
466
+ negativePrompt: () => [
467
+ "manga",
468
+ "anime",
469
+ "american comic",
470
+ "grayscale",
471
+ "monochrome",
472
+ "painting"
473
+ ],
474
+ },
475
+ */
476
+ egyptian: {
477
+ id: "egyptian",
478
+ label: "Egyptian",
479
+ family: "european",
480
+ color: "color",
481
+ font: "actionman",
482
+ llmPrompt: "new movie",
483
+ imagePrompt: (prompt: string) => [
484
+ `ancient egyptian wall painting`,
485
+ // `medieval color engraving`,
486
+ `${prompt}`,
487
+ `ancient egypt`,
488
+ ],
489
+ negativePrompt: () => [
490
+ "manga",
491
+ "anime",
492
+ "american comic",
493
+ "grayscale",
494
+ "monochrome",
495
+ "painting"
496
+ ],
497
+ },
498
+ /*
499
+ psx: {
500
+ label: "PSX",
501
+ family: "european",
502
+ color: "color",
503
+ font: "actionman",
504
+ llmPrompt: "new movie",
505
+ imagePrompt: (prompt: string) => [
506
+ `videogame screenshot`,
507
+ `3dfx`,
508
+ `3D dos game`,
509
+ `software rendering`,
510
+ `${prompt}`,
511
+ ],
512
+ negativePrompt: () => [
513
+ "manga",
514
+ "anime",
515
+ "american comic",
516
+ "grayscale",
517
+ "monochrome",
518
+ "painting"
519
+ ],
520
+ },
521
+ */
522
+ /*
523
+ pixel: {
524
+ label: "Pixel",
525
+ family: "european",
526
+ color: "color",
527
+ font: "actionman",
528
+ llmPrompt: "new movie",
529
+ imagePrompt: (prompt: string) => [
530
+ `pixelart`,
531
+ `isometric`,
532
+ `pixelated`,
533
+ `low res`,
534
+ `${prompt}`,
535
+ ],
536
+ negativePrompt: () => [
537
+ "manga",
538
+ "anime",
539
+ "american comic",
540
+ "grayscale",
541
+ "monochrome",
542
+ "painting"
543
+ ],
544
+ },
545
+ */
546
+ }
547
+
548
+ export type PresetName = keyof typeof presets
549
+
550
+ export const defaultPreset: PresetName = "american_comic_90"
551
+
552
+ export const nonRandomPresets = Object.keys(presets).filter(p => p !== "random")
553
+
554
+ export const getPreset = (preset?: PresetName): Preset => presets[preset || defaultPreset] || presets[defaultPreset]
555
+
556
+ export const getRandomPreset = (): Preset => {
557
+ const presetName = pick(Object.keys(presets).filter(preset => preset !== "random")) as PresetName
558
+ return getPreset(presetName)
559
+ }
src/app/engine/render.ts ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server"
2
+
3
+ import Replicate, { Prediction } from "replicate"
4
+
5
+ import { RenderRequest, RenderedScene, RenderingEngine } from "@/types"
6
+ import { generateSeed } from "@/lib/generateSeed"
7
+ import { sleep } from "@/lib/sleep"
8
+
9
+ const renderingEngine = `${process.env.RENDERING_ENGINE || ""}` as RenderingEngine
10
+
11
+ const replicateToken = `${process.env.REPLICATE_API_TOKEN || ""}`
12
+ const replicateModel = `${process.env.REPLICATE_API_MODEL || ""}`
13
+ const replicateModelVersion = `${process.env.REPLICATE_API_MODEL_VERSION || ""}`
14
+
15
+ // note: there is no / at the end in the variable
16
+ // so we have to add it ourselves if needed
17
+ const apiUrl = process.env.VIDEOCHAIN_API_URL
18
+
19
+ export async function newRender({
20
+ prompt,
21
+ // negativePrompt,
22
+ width,
23
+ height
24
+ }: {
25
+ prompt: string
26
+ // negativePrompt: string[]
27
+ width: number
28
+ height: number
29
+ }) {
30
+ // console.log(`newRender(${prompt})`)
31
+ if (!prompt) {
32
+ console.error(`cannot call the rendering API without a prompt, aborting..`)
33
+ throw new Error(`cannot call the rendering API without a prompt, aborting..`)
34
+ }
35
+
36
+ let defaulResult: RenderedScene = {
37
+ renderId: "",
38
+ status: "error",
39
+ assetUrl: "",
40
+ alt: prompt || "",
41
+ maskUrl: "",
42
+ error: "failed to fetch the data",
43
+ segments: []
44
+ }
45
+
46
+
47
+ try {
48
+ if (renderingEngine === "REPLICATE") {
49
+ if (!replicateToken) {
50
+ throw new Error(`you need to configure your REPLICATE_API_TOKEN in order to use the REPLICATE rendering engine`)
51
+ }
52
+ if (!replicateModel) {
53
+ throw new Error(`you need to configure your REPLICATE_API_MODEL in order to use the REPLICATE rendering engine`)
54
+ }
55
+ if (!replicateModelVersion) {
56
+ throw new Error(`you need to configure your REPLICATE_API_MODEL_VERSION in order to use the REPLICATE rendering engine`)
57
+ }
58
+ const replicate = new Replicate({ auth: replicateToken })
59
+
60
+ // console.log("Calling replicate..")
61
+ const seed = generateSeed()
62
+ const prediction = await replicate.predictions.create({
63
+ version: replicateModelVersion,
64
+ input: { prompt, seed }
65
+ })
66
+
67
+ // console.log("prediction:", prediction)
68
+
69
+ // no need to reply straight away: good things take time
70
+ // also our friends at Replicate won't like it if we spam them with requests
71
+ await sleep(4000)
72
+
73
+ return {
74
+ renderId: prediction.id,
75
+ status: "pending",
76
+ assetUrl: "",
77
+ alt: prompt,
78
+ error: prediction.error,
79
+ maskUrl: "",
80
+ segments: []
81
+ } as RenderedScene
82
+ } else {
83
+ // console.log(`calling POST ${apiUrl}/render with prompt: ${prompt}`)
84
+ const res = await fetch(`${apiUrl}/render`, {
85
+ method: "POST",
86
+ headers: {
87
+ Accept: "application/json",
88
+ "Content-Type": "application/json",
89
+ Authorization: `Bearer ${process.env.VIDEOCHAIN_API_TOKEN}`,
90
+ },
91
+ body: JSON.stringify({
92
+ prompt,
93
+ // negativePrompt, unused for now
94
+ nbFrames: 1,
95
+ nbSteps: 25, // 20 = fast, 30 = better, 50 = best
96
+ actionnables: [], // ["text block"],
97
+ segmentation: "disabled", // "firstframe", // one day we will remove this param, to make it automatic
98
+ width,
99
+ height,
100
+
101
+ // no need to upscale right now as we generate tiny panels
102
+ // maybe later we can provide an "export" button to PDF
103
+ // unfortunately there are too many requests for upscaling,
104
+ // the server is always down
105
+ upscalingFactor: 1, // 2,
106
+
107
+ // analyzing doesn't work yet, it seems..
108
+ analyze: false, // analyze: true,
109
+
110
+ cache: "ignore"
111
+ } as Partial<RenderRequest>),
112
+ cache: 'no-store',
113
+ // we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
114
+ // next: { revalidate: 1 }
115
+ })
116
+
117
+
118
+ // console.log("res:", res)
119
+ // The return value is *not* serialized
120
+ // You can return Date, Map, Set, etc.
121
+
122
+ // Recommendation: handle errors
123
+ if (res.status !== 200) {
124
+ // This will activate the closest `error.js` Error Boundary
125
+ throw new Error('Failed to fetch data')
126
+ }
127
+
128
+ const response = (await res.json()) as RenderedScene
129
+ return response
130
+ }
131
+ } catch (err) {
132
+ console.error(err)
133
+ return defaulResult
134
+ }
135
+ }
136
+
137
+ export async function getRender(renderId: string) {
138
+ if (!renderId) {
139
+ console.error(`cannot call the rendering API without a renderId, aborting..`)
140
+ throw new Error(`cannot call the rendering API without a renderId, aborting..`)
141
+ }
142
+
143
+ let defaulResult: RenderedScene = {
144
+ renderId: "",
145
+ status: "pending",
146
+ assetUrl: "",
147
+ alt: "",
148
+ maskUrl: "",
149
+ error: "failed to fetch the data",
150
+ segments: []
151
+ }
152
+
153
+ try {
154
+ if (renderingEngine === "REPLICATE") {
155
+ if (!replicateToken) {
156
+ throw new Error(`you need to configure your REPLICATE_API_TOKEN in order to use the REPLICATE rendering engine`)
157
+ }
158
+ if (!replicateModel) {
159
+ throw new Error(`you need to configure your REPLICATE_API_MODEL in order to use the REPLICATE rendering engine`)
160
+ }
161
+
162
+ // const replicate = new Replicate({ auth: replicateToken })
163
+
164
+ // console.log("Calling replicate..")
165
+ // const prediction = await replicate.predictions.get(renderId)
166
+ // console.log("Prediction:", prediction)
167
+
168
+ // console.log(`calling GET https://api.replicate.com/v1/predictions/${renderId}`)
169
+ const res = await fetch(`https://api.replicate.com/v1/predictions/${renderId}`, {
170
+ method: "GET",
171
+ headers: {
172
+ // Accept: "application/json",
173
+ // "Content-Type": "application/json",
174
+ Authorization: `Token ${replicateToken}`,
175
+ },
176
+ cache: 'no-store',
177
+ // we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
178
+ // next: { revalidate: 1 }
179
+ })
180
+
181
+ // console.log("res:", res)
182
+ // The return value is *not* serialized
183
+ // You can return Date, Map, Set, etc.
184
+
185
+ // Recommendation: handle errors
186
+ if (res.status !== 200) {
187
+ // This will activate the closest `error.js` Error Boundary
188
+ throw new Error('Failed to fetch data')
189
+ }
190
+
191
+ const response = (await res.json()) as any
192
+ // console.log("response:", response)
193
+
194
+ return {
195
+ renderId,
196
+ status: response?.error ? "error" : response?.status === "succeeded" ? "completed" : "pending",
197
+ assetUrl: `${response?.output || ""}`,
198
+ alt: `${response?.input?.prompt || ""}`,
199
+ error: `${response?.error || ""}`,
200
+ maskUrl: "",
201
+ segments: []
202
+ } as RenderedScene
203
+ } else {
204
+ // console.log(`calling GET ${apiUrl}/render with renderId: ${renderId}`)
205
+ const res = await fetch(`${apiUrl}/render/${renderId}`, {
206
+ method: "GET",
207
+ headers: {
208
+ Accept: "application/json",
209
+ "Content-Type": "application/json",
210
+ Authorization: `Bearer ${process.env.VIDEOCHAIN_API_TOKEN}`,
211
+ },
212
+ cache: 'no-store',
213
+ // we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
214
+ // next: { revalidate: 1 }
215
+ })
216
+
217
+ // console.log("res:", res)
218
+ // The return value is *not* serialized
219
+ // You can return Date, Map, Set, etc.
220
+
221
+ // Recommendation: handle errors
222
+ if (res.status !== 200) {
223
+ // This will activate the closest `error.js` Error Boundary
224
+ throw new Error('Failed to fetch data')
225
+ }
226
+
227
+ const response = (await res.json()) as RenderedScene
228
+ // console.log("response:", response)
229
+ return response
230
+ }
231
+ } catch (err) {
232
+ console.error(err)
233
+ defaulResult.status = "error"
234
+ defaulResult.error = `${err}`
235
+ // Gorgon.clear(cacheKey)
236
+ return defaulResult
237
+ }
238
+
239
+ // }, cacheDurationInSec * 1000)
240
+ }
241
+
242
+ export async function upscaleImage(image: string): Promise<{
243
+ assetUrl: string
244
+ error: string
245
+ }> {
246
+ if (!image) {
247
+ console.error(`cannot call the rendering API without an image, aborting..`)
248
+ throw new Error(`cannot call the rendering API without an image, aborting..`)
249
+ }
250
+
251
+ let defaulResult = {
252
+ assetUrl: "",
253
+ error: "failed to fetch the data",
254
+ }
255
+
256
+ try {
257
+ // console.log(`calling GET ${apiUrl}/render with renderId: ${renderId}`)
258
+ const res = await fetch(`${apiUrl}/upscale`, {
259
+ method: "POST",
260
+ headers: {
261
+ Accept: "application/json",
262
+ "Content-Type": "application/json",
263
+ Authorization: `Bearer ${process.env.VIDEOCHAIN_API_TOKEN}`,
264
+ },
265
+ cache: 'no-store',
266
+ body: JSON.stringify({ image, factor: 3 })
267
+ // we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
268
+ // next: { revalidate: 1 }
269
+ })
270
+
271
+ // console.log("res:", res)
272
+ // The return value is *not* serialized
273
+ // You can return Date, Map, Set, etc.
274
+
275
+ // Recommendation: handle errors
276
+ if (res.status !== 200) {
277
+ // This will activate the closest `error.js` Error Boundary
278
+ throw new Error('Failed to fetch data')
279
+ }
280
+
281
+ const response = (await res.json()) as {
282
+ assetUrl: string
283
+ error: string
284
+ }
285
+ // console.log("response:", response)
286
+ return response
287
+ } catch (err) {
288
+ console.error(err)
289
+ // Gorgon.clear(cacheKey)
290
+ return defaulResult
291
+ }
292
+
293
+ // }, cacheDurationInSec * 1000)
294
+ }