Skip to content

Commit

Permalink
refactor: migrate to Next.js
Browse files Browse the repository at this point in the history
  • Loading branch information
LeafYeeXYZ committed Nov 19, 2024
1 parent faa1835 commit c606690
Show file tree
Hide file tree
Showing 61 changed files with 921 additions and 1,877 deletions.
22 changes: 0 additions & 22 deletions .eslintrc.cjs

This file was deleted.

3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}
58 changes: 37 additions & 21 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,24 +1,40 @@
# Logs
logs
*.log
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
50 changes: 28 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,45 @@
**[简体中文](README_ZH_CN.md) | [正體中文](README_ZH_TW.md) | English**
**For old version, see `v3` branch.**

# PainterLeaf
- Text-to-image, supports multiple models
- Image-to-text, convert local images to prompts
- Also supports image-to-image
# Painter Leaf

- Text-to-image: supports multiple models
- Image-to-text: convert local images to prompts
- Prompt supports Chinese and English (Chinese will automatically call `AI` translation)
- Front-end and back-end separation, front-end based on `React`, back-end based on `Hono`, see [this project](https://github.com/LeafYeeXYZ/MyAPIs)
- `API` provided by `CloudflareAI` and `HuggingFace`
- Internationalization support, currently supports `简体中文`, `正體中文`, and `English`

|![](./readme/mobile-light.jpeg)|![](./readme/mobile-dark.jpeg)|
|:---:|:---:|
|![](./readme/light.png)|![](./readme/dark.png)|
## TODO

- [ ] Implement `Image-to-text` feature
- [ ] Add preview images to `README.md`

## Usage
### Deploy Server
See [this project](https://github.com/LeafYeeXYZ/MyAPIs)

### Set Server URL
Set `VITE_SERVER` environment variable in `.env` file, `Vercel` or `Cloudflare Pages`, such as `https://api.xxx.workers.dev`
### Config Environment Variables

Set following environment variables in `.env` file or `Vercel`:

### Install Bun
Please refer to [Bun.sh](https://bun.sh). Or simply run `npm i -g bun`
| Key | Value | Required |
| :---: | :---: | :---: |
| `CF_USER_ID` | `Cloudflare` user id | ✅ for `Cloudflare AI` |
| `CF_AI_API_KEY` | `Cloudflare AI` api key | ✅ for `Cloudflare AI` |
| `HF_API_KEY` | `HuggingFace` api key | ✅ for `HuggingFace` |

> If you don't need specific provider, you can leave the key empty.
### Install dependencies

```bash
bun i
```

### Local run
```bash
bun run dev
```
> If you haven't installed `Bun` yet, please refer to [Bun.sh](https://bun.sh).
### Local Development

### Build
```bash
bun run build
bun dev
```

### Deploy

It's recommended to deploy to `Vercel` while you can also deploy to other platforms, just make sure to set the environment variables correctly.
39 changes: 0 additions & 39 deletions README_ZH_CN.md

This file was deleted.

39 changes: 0 additions & 39 deletions README_ZH_TW.md

This file was deleted.

93 changes: 93 additions & 0 deletions app/api/image/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/** 生成请求地址和请求选项 */
class PainterRequest {
#cfReg = /^@cf\//
#hfReg = /^@hf\//
url: string
options: {
method: string
headers: HeadersInit
body: string
}
constructor(
model: string,
prompt: string,
env: { CF_USER_ID: string, CF_AI_API_KEY: string, HF_API_KEY: string },
) {
// 判断模型
if (this.#cfReg.test(model)) {
// Cloudflare
this.url = `https://api.cloudflare.com/client/v4/accounts/${env.CF_USER_ID}/ai/run/${model}`
this.options = {
method: 'POST',
headers: {
'content-type': 'application/json',
'Authorization': `Bearer ${env.CF_AI_API_KEY}`
},
body: JSON.stringify({
prompt: prompt,
negative_prompt: 'lowres, bad, text, error, missing, extra, fewer, cropped, jpeg artifacts, worst quality, bad quality, watermark, bad aesthetic, unfinished, chromatic aberration, scan, scan artifacts',
})
}
// 针对 Cloudflare 的 FLUX.1 Schnell 模型的特殊处理
if (model === '@cf/black-forest-labs/flux-1-schnell') {
this.options.body = JSON.stringify({
num_steps: 8,
...JSON.parse(this.options.body)
})
}
} else if (this.#hfReg.test(model)) {
// Hugging Face
this.url = `https://api-inference.huggingface.co/models/${model.replace(this.#hfReg, '')}`
this.options = {
method: 'POST',
headers: {
'content-type': 'application/json',
'Authorization': `Bearer ${env.HF_API_KEY}`
},
body: JSON.stringify({
inputs: prompt,
prompt: prompt,
negative_prompt: 'lowres, bad, text, error, missing, extra, fewer, cropped, jpeg artifacts, worst quality, bad quality, watermark, bad aesthetic, unfinished, chromatic aberration, scan, scan artifacts',
})
}
} else {
throw new Error(`Unsupported model: ${model}`)
}
}
}

export async function POST(req: Request): Promise<Response> {
try {
// 图片
const { model, prompt } = await req.json()
// 请求参数和请求地址
const { options, url } = new PainterRequest(model, prompt, {
CF_USER_ID: process.env.CF_USER_ID ?? '',
CF_AI_API_KEY: process.env.CF_AI_API_KEY ?? '',
HF_API_KEY: process.env.HF_API_KEY ?? '',
})
// 发送请求
const response = await fetch(url, options)
// 针对 Cloudflare 的 FLUX.1 Schnell 模型的特殊处理
if (model === '@cf/black-forest-labs/flux-1-schnell') {
const res = await response.json()
const base64 = res.result.image as string
const buffer = new Uint8Array(atob(base64).split('').map(c => c.charCodeAt(0)))
return new Response(buffer, {
status: response.status,
headers: {
'content-type': 'image/png',
}
})
}
// 返回结果
return new Response(response.body, {
status: response.status,
headers: {
'content-type': response.headers.get('content-type') ?? 'text/plain',
}
})
} catch (e) {
return new Response(e instanceof Error ? e.message : 'Unkown Server Error', { status: 500 })
}
}
37 changes: 37 additions & 0 deletions app/api/prompt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

// import type { Context } from 'hono'
// export async function painter_genprompt(c: Context): Promise<Response> {
// try {
// // 请求参数
// const url = `https://api.cloudflare.com/client/v4/accounts/${c.env.CF_USER}/ai/run/@cf/unum/uform-gen2-qwen-500m`
// const req = await c.req.json()
// const body = {
// image: req.image,
// max_tokens: 2048,
// prompt: 'Generate a detailed description in a single paragraph for this image',
// }
// // 发送请求
// const response = await fetch(url, {
// method: 'POST',
// headers: {
// 'content-type': 'application/json',
// 'Authorization': `Bearer ${c.env.CF_AI_API_KEY}`
// },
// body: JSON.stringify(body)
// })
// const result = await response.json()
// // 返回结果
// console.log(SUCCESS_MESSAGE)
// return new Response(JSON.stringify(result), {
// status: result.success ? 200 : 500,
// headers: {
// 'content-type': 'application/json',
// }
// })
// } catch(e) {
// const message = e instanceof Error ? e.message : 'Unkown Server Error'
// ERROR_MESSAGE.data!.error = message
// console.error(ERROR_MESSAGE)
// return new Response(message, { status: 500 })
// }
// }
26 changes: 26 additions & 0 deletions app/api/translate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export async function POST(req: Request): Promise<Response> {
try {
const url = `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_USER_ID}/ai/run/@cf/meta/m2m100-1.2b`
const { zh } = await req.json()
const res = await fetch(url, {
method: 'POST',
headers: {
'content-type': 'application/json',
'Authorization': `Bearer ${process.env.CF_AI_API_KEY}`
},
body: JSON.stringify({
text: zh,
source_lang: 'zh',
target_lang: 'en',
}),
})
const result = await res.json()
if (result?.success) {
return Response.json(result)
} else {
return new Response('Translate Failed', { status: 500 })
}
} catch(e) {
return new Response(e instanceof Error ? e.message : 'Unkown Server Error', { status: 500 })
}
}
Loading

0 comments on commit c606690

Please sign in to comment.