Spaces:
Runtime error
Runtime error
Commit
·
84d81c2
0
Parent(s):
Duplicate from jbilcke-hf/ai-comic-factory
Browse filesCo-authored-by: Julian Bilcke <[email protected]>
This view is limited to 50 files because it contains too many changes.
See raw diff
- .env +40 -0
- .eslintrc.json +3 -0
- .gitignore +35 -0
- .nvmrc +1 -0
- Dockerfile +65 -0
- README.md +128 -0
- components.json +16 -0
- next.config.js +11 -0
- package-lock.json +0 -0
- package.json +71 -0
- postcss.config.js +6 -0
- public/bubble.jpg +0 -0
- public/favicon.ico +0 -0
- public/favicon/Icon/r +0 -0
- public/favicon/favicon-114-precomposed.png +0 -0
- public/favicon/favicon-120-precomposed.png +0 -0
- public/favicon/favicon-144-precomposed.png +0 -0
- public/favicon/favicon-152-precomposed.png +0 -0
- public/favicon/favicon-180-precomposed.png +0 -0
- public/favicon/favicon-192.png +0 -0
- public/favicon/favicon-32.png +0 -0
- public/favicon/favicon-36.png +0 -0
- public/favicon/favicon-48.png +0 -0
- public/favicon/favicon-57.png +0 -0
- public/favicon/favicon-60.png +0 -0
- public/favicon/favicon-72-precomposed.png +0 -0
- public/favicon/favicon-72.png +0 -0
- public/favicon/favicon-76.png +0 -0
- public/favicon/favicon-96.png +0 -0
- public/favicon/favicon.ico +0 -0
- public/favicon/index.html +133 -0
- public/favicon/manifest.json +41 -0
- public/icon.png +0 -0
- public/layouts/layout0.jpg +0 -0
- public/layouts/layout0_hd.jpg +0 -0
- public/layouts/layout1.jpg +0 -0
- public/layouts/layout1_hd.jpg +0 -0
- public/layouts/layout2.jpg +0 -0
- public/layouts/layout2_hd.jpg +0 -0
- public/layouts/layout3 hd.jpg +0 -0
- public/layouts/layout3.jpg +0 -0
- public/mask.png +0 -0
- public/next.svg +1 -0
- public/vercel.svg +1 -0
- src/app/engine/caption.ts +54 -0
- src/app/engine/censorship.ts +39 -0
- src/app/engine/community.ts +135 -0
- src/app/engine/forbidden.ts +6 -0
- src/app/engine/presets.ts +559 -0
- 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"><!-- For old IEs --></span>
|
84 |
+
<link rel="shortcut icon" href="favicon.ico" />
|
85 |
+
|
86 |
+
<span class="comment"><!-- For new browsers - multisize ico --></span>
|
87 |
+
<link rel="icon" type="image/x-icon" sizes="16x16 32x32" href="favicon.ico">
|
88 |
+
|
89 |
+
<span class="comment"><!-- For iPad with high-resolution Retina display running iOS ≥ 7: --></span>
|
90 |
+
<link rel="apple-touch-icon" sizes="152x152" href="favicon-152-precomposed.png">
|
91 |
+
|
92 |
+
<span class="comment"><!-- For iPad with high-resolution Retina display running iOS ≤ 6: --></span>
|
93 |
+
<link rel="apple-touch-icon" sizes="144x144" href="favicon-144-precomposed.png">
|
94 |
+
|
95 |
+
<span class="comment"><!-- For iPhone with high-resolution Retina display running iOS ≥ 7: --></span>
|
96 |
+
<link rel="apple-touch-icon" sizes="120x120" href="favicon-120-precomposed.png">
|
97 |
+
|
98 |
+
<span class="comment"><!-- For iPhone with high-resolution Retina display running iOS ≤ 6: --></span>
|
99 |
+
<link rel="apple-touch-icon" sizes="114x114" href="favicon-114-precomposed.png">
|
100 |
+
|
101 |
+
<span class="comment"><!-- For iPhone 6+ --></span>
|
102 |
+
<link rel="apple-touch-icon" sizes="180x180" href="favicon-180-precomposed.png">
|
103 |
+
|
104 |
+
<span class="comment"><!-- For first- and second-generation iPad: --></span>
|
105 |
+
<link rel="apple-touch-icon" sizes="72x72" href="favicon-72-precomposed.png">
|
106 |
+
|
107 |
+
<span class="comment"><!-- For non-Retina iPhone, iPod Touch, and Android 2.1+ devices: --></span>
|
108 |
+
<link rel="apple-touch-icon" sizes="57x57" href="favicon-57.png">
|
109 |
+
|
110 |
+
<span class="comment"><!-- For Old Chrome --></span>
|
111 |
+
<link rel="icon" sizes="32x32" href="favicon-32.png" >
|
112 |
+
|
113 |
+
<span class="comment"><!-- For IE10 Metro --></span>
|
114 |
+
<meta name="msapplication-TileColor" content="#FFFFFF">
|
115 |
+
<meta name="msapplication-TileImage" content="favicon-144.png">
|
116 |
+
<meta name="theme-color" content="#ffffff">
|
117 |
+
|
118 |
+
<span class="comment"><!-- Chrome for Android --></span>
|
119 |
+
<link rel="manifest" href="manifest.json">
|
120 |
+
<link rel="icon" sizes="192x192" href="favicon-192.png">
|
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 |
+
}
|