mirror of
https://github.com/xzeldon/fooocos-telegram-bot
synced 2024-12-25 05:15:47 +00:00
Initial commit
This commit is contained in:
commit
a9613544b7
4
.env.sample
Normal file
4
.env.sample
Normal file
@ -0,0 +1,4 @@
|
||||
NODE_ENV=development
|
||||
BOT_TOKEN=
|
||||
FOOOCOS_API_URL=
|
||||
DUMMY_CHAT_ID=
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
.vscode
|
||||
node_modules
|
||||
.env
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Timofey Gelazoniya
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
137
README.md
Normal file
137
README.md
Normal file
@ -0,0 +1,137 @@
|
||||
# fooocos-telegram-bot
|
||||
|
||||
![bot](static/gorilla.gif)
|
||||
|
||||
#### ⚠️ The project may contain some bugs and errors, provided as is
|
||||
|
||||
Dynamic inline bot leveraging the robust capabilities of Fooocos for image generation. This bot integrates seamlessly with Telegram, offering an intuitive and engaging user experience.
|
||||
|
||||
Current [FooocosAPI](https://github.com/konieshadow/Fooocus-API/tree/bf2f2c745a159e13b8de93fd000470ed98c973c4) commit hash: `bf2f2c745a159e13b8de93fd000470ed98c973c4`
|
||||
|
||||
> Mirror on my [<img src="https://git.zeldon.ru/assets/img/logo.svg" align="center" width="20" height="20"/> Git](https://git.zeldon.ru/zeldon/fooocos-telegram-bot.git)
|
||||
|
||||
# Getting Started
|
||||
|
||||
## System Requirements
|
||||
Before diving in, ensure your system meets these prerequisites:
|
||||
|
||||
- Git
|
||||
- Python >= 3.10
|
||||
- NodeJS >= 20
|
||||
- An Nvidia GPU with a minimum of 4GB VRAM (other GPUs may be supported; check the [official Fooocos repository](https://github.com/konieshadow/Fooocus-API) for details).
|
||||
|
||||
## Installation Guide for Windows
|
||||
|
||||
> This guide for **Windows** only, but for Linux the logic is exactly the same, but the commands may be slightly different
|
||||
|
||||
### Install the correct FooocosAPI version locally:
|
||||
|
||||
1. Clone the FooocosAPI repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/konieshadow/Fooocus-API/tree/bf2f2c745a159e13b8de93fd000470ed98c973c4
|
||||
```
|
||||
2. Navigate to the cloned directory:
|
||||
|
||||
```bash
|
||||
cd Fooocus-API
|
||||
```
|
||||
3. Create and activate a Python virtual environment:
|
||||
|
||||
```bash
|
||||
python -m venv venv
|
||||
.\venv\Scripts\activate
|
||||
```
|
||||
|
||||
4. Install requirements:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
5. Launch FooocosAPI:
|
||||
|
||||
```bash
|
||||
python .\main.py --host 0.0.0.0
|
||||
```
|
||||
If successful, you should see output similar to this:
|
||||
|
||||
![output](static/output.png)
|
||||
|
||||
For subsequent runs, simply activate the virtual environment and start the API:
|
||||
|
||||
```bash
|
||||
.\venv\Scripts\activate
|
||||
python .\main.py --host 0.0.0.0
|
||||
```
|
||||
|
||||
> Tip: You can also try the [latest FooocosAPI version](https://github.com/konieshadow/Fooocus-API), but you might need to regenerate the client (`npm run fooocos:typegen`) and address any TypeScript compiler errors.
|
||||
|
||||
### Setting Up the Bot:
|
||||
|
||||
1. Clone this repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/xzeldon/fooocos-telegram-bot.git
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
npm i
|
||||
```
|
||||
|
||||
3. Prepare your configuration file:
|
||||
|
||||
- Duplicate the `.env.sample` file and rename it to `.env`.
|
||||
- Edit the `.env` file with your specific details as per the table below:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>NODE_ENV</td>
|
||||
<td>String</td>
|
||||
<td>Specifies the application environment. (<code>development</code> or <code>production</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>BOT_TOKEN</td>
|
||||
<td>
|
||||
String
|
||||
</td>
|
||||
<td>
|
||||
Telegram Bot API token obtained from <a href="https://t.me/BotFather">@BotFather</a>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>FOOOCOS_API_URL</td>
|
||||
<td>String</td>
|
||||
<td>URL of the Fooocos-API endpoint.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DUMMY_CHAT_ID</td>
|
||||
<td>String</td>
|
||||
<td>ID of a dummy chat for uploading images to retrieve their file_id for inline message editing.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
4. Start the bot:
|
||||
|
||||
- For development with hot reload:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
- For standard operation:
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
# License
|
||||
This project is open-source, licensed under the MIT License. Feel free to use, modify, and distribute it as you see fit.
|
6
fooocos/client.ts
Normal file
6
fooocos/client.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import createClient from "openapi-fetch";
|
||||
import { paths } from './schema.js';
|
||||
|
||||
export const fooocos = createClient<paths>({
|
||||
baseUrl: process.env["FOOOCOS_API_URL"]
|
||||
});
|
1814
fooocos/openapi.json
Normal file
1814
fooocos/openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
1121
fooocos/schema.d.ts
vendored
Normal file
1121
fooocos/schema.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
16
index.ts
Normal file
16
index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { createBot } from "#root/bot.js";
|
||||
import { logger } from "#root/logger.js";
|
||||
import { run } from "@grammyjs/runner";
|
||||
|
||||
const main = async () => {
|
||||
try {
|
||||
const bot = createBot(process.env["BOT_TOKEN"]!);
|
||||
await bot.init();
|
||||
run(bot);
|
||||
logger.info(`Bot @${bot.botInfo.username} (id = ${bot.botInfo.id}) is up and running...`);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
main();
|
2005
package-lock.json
generated
Normal file
2005
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
package.json
Normal file
33
package.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "fooocos-telegram-bot",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"imports": {
|
||||
"#root/*": "./dist/src/*"
|
||||
},
|
||||
"scripts": {
|
||||
"fooocos:typegen": "openapi-typescript ./fooocos/openapi.json -o ./fooocos/schema.d.ts",
|
||||
"dev": "npm run clean && tsc-watch --onSuccess \"tsx --env-file=.env index.ts\"",
|
||||
"start": "tsx --env-file=.env index.ts",
|
||||
"build": "npm run clean && tsc --noEmit false",
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.9.2",
|
||||
"openapi-typescript": "^6.7.1",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsc-watch": "^6.0.4",
|
||||
"tsx": "^4.1.4",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grammyjs/runner": "^2.0.3",
|
||||
"@grammyjs/transformer-throttler": "^1.2.1",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"grammy": "^1.19.2",
|
||||
"openapi-fetch": "^0.8.1",
|
||||
"p-queue": "^7.4.1",
|
||||
"pino": "^8.16.2",
|
||||
"pino-pretty": "^10.2.3"
|
||||
}
|
||||
}
|
62
src/body.ts
Normal file
62
src/body.ts
Normal file
@ -0,0 +1,62 @@
|
||||
export const textToImageBody = (prompt: string) => {
|
||||
return {
|
||||
"prompt": prompt,
|
||||
"negative_prompt": "",
|
||||
"style_selections": [
|
||||
"Fooocus V2",
|
||||
"Fooocus Enhance",
|
||||
"Fooocus Sharp"
|
||||
],
|
||||
// "Speed" | "Quality" | "Extreme Speed"
|
||||
"performance_selection": "Speed",
|
||||
"aspect_ratios_selection": "1152×896",
|
||||
"image_number": 1,
|
||||
"image_seed": -1,
|
||||
"sharpness": 2,
|
||||
"guidance_scale": 4,
|
||||
"base_model_name": "juggernautXL_version6Rundiffusion.safetensors",
|
||||
"refiner_model_name": "None",
|
||||
"refiner_switch": 0.5,
|
||||
"loras": [
|
||||
{
|
||||
"model_name": "sd_xl_offset_example-lora_1.0.safetensors",
|
||||
"weight": 0.1
|
||||
}
|
||||
],
|
||||
"advanced_params": {
|
||||
"disable_preview": false,
|
||||
"adm_scaler_positive": 1.5,
|
||||
"adm_scaler_negative": 0.8,
|
||||
"adm_scaler_end": 0.3,
|
||||
"refiner_swap_method": "joint",
|
||||
"adaptive_cfg": 7,
|
||||
"sampler_name": "dpmpp_2m_sde_gpu",
|
||||
"scheduler_name": "karras",
|
||||
"overwrite_step": -1,
|
||||
"overwrite_switch": -1,
|
||||
"overwrite_width": -1,
|
||||
"overwrite_height": -1,
|
||||
"overwrite_vary_strength": -1,
|
||||
"overwrite_upscale_strength": -1,
|
||||
"mixing_image_prompt_and_vary_upscale": false,
|
||||
"mixing_image_prompt_and_inpaint": false,
|
||||
"debugging_cn_preprocessor": false,
|
||||
"skipping_cn_preprocessor": false,
|
||||
"controlnet_softness": 0.25,
|
||||
"canny_low_threshold": 64,
|
||||
"canny_high_threshold": 128,
|
||||
"freeu_enabled": false,
|
||||
"freeu_b1": 1.01,
|
||||
"freeu_b2": 1.02,
|
||||
"freeu_s1": 0.99,
|
||||
"freeu_s2": 0.95,
|
||||
"debugging_inpaint_preprocessor": false,
|
||||
"inpaint_disable_initial_latent": false,
|
||||
"inpaint_engine": "v1",
|
||||
"inpaint_strength": 1,
|
||||
"inpaint_respective_field": 1
|
||||
},
|
||||
"require_base64": false,
|
||||
"async_process": true
|
||||
};
|
||||
};
|
99
src/bot.ts
Normal file
99
src/bot.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { logger } from "#root/logger.js";
|
||||
import { predict } from "#root/predict.js";
|
||||
import { downloadImage, isFooocosAlive, uploadPhoto } from "#root/utils.js";
|
||||
import { apiThrottler } from "@grammyjs/transformer-throttler";
|
||||
import { Bot, Context, InlineKeyboard, InlineQueryResultBuilder, InputMediaBuilder } from "grammy";
|
||||
|
||||
const activeTasks = new Map<number, boolean>();
|
||||
|
||||
const articlePhoto = {
|
||||
url: "https://i.imgur.com/oAWNcVt.jpg",
|
||||
width: 512,
|
||||
height: 512
|
||||
};
|
||||
|
||||
async function handleInlineQuery(ctx: Context) {
|
||||
if (!await isFooocosAlive()) {
|
||||
const answer = InlineQueryResultBuilder.article('fooocos:down', "Сервис недоступен")
|
||||
.text('Сервис временно недоступен. Попробуйте позже');
|
||||
return ctx.answerInlineQuery([answer], { cache_time: 0 });
|
||||
}
|
||||
|
||||
const userId = Number(ctx.update.inline_query?.from.id);
|
||||
const prompt = ctx.inlineQuery?.query;
|
||||
|
||||
if (activeTasks.get(userId)) {
|
||||
logger.trace(`${userId} in activeTasks`);
|
||||
return ctx.answerInlineQuery([]);
|
||||
}
|
||||
|
||||
const answer = prompt ?
|
||||
InlineQueryResultBuilder.photo(`fooocos:query:prompt`, articlePhoto.url, {
|
||||
caption: prompt,
|
||||
thumbnail_url: articlePhoto.url,
|
||||
photo_width: articlePhoto.width,
|
||||
photo_height: articlePhoto.height,
|
||||
reply_markup: new InlineKeyboard()
|
||||
.switchInlineCurrent('Вы в очереди')
|
||||
}) :
|
||||
InlineQueryResultBuilder.article('fooocos:query', 'введите промпт', {
|
||||
reply_markup: new InlineKeyboard()
|
||||
.switchInlineCurrent('Попробовать снова')
|
||||
})
|
||||
.text('Нет промпта');
|
||||
|
||||
return ctx.answerInlineQuery([answer], { cache_time: 0 });
|
||||
}
|
||||
|
||||
async function handleChosenInlineResult(ctx: Context) {
|
||||
const userId = ctx.update.chosen_inline_result?.from.id!;
|
||||
const messageId = ctx.chosenInlineResult?.inline_message_id!;
|
||||
const prompt = ctx.chosenInlineResult?.query!;
|
||||
|
||||
if (!prompt) {
|
||||
return ctx.api.editMessageTextInline(messageId, "Ошибка: нет промпта. Попробуй ещё раз!");
|
||||
}
|
||||
|
||||
activeTasks.set(userId, true);
|
||||
|
||||
const gen = predict(prompt);
|
||||
|
||||
for await (const state of gen) {
|
||||
if (state.status === "RUNNING") {
|
||||
if (state.preview) {
|
||||
const previewImage = Buffer.from(state.preview, 'base64');
|
||||
const fileId = await uploadPhoto(ctx, previewImage);
|
||||
const media = InputMediaBuilder.photo(fileId, {
|
||||
caption: `Промпт: ${prompt}`
|
||||
});
|
||||
|
||||
try {
|
||||
await ctx.api.editMessageMediaInline(messageId, media);
|
||||
} catch (err) {
|
||||
activeTasks.delete(userId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (state.status === "SUCCESS") {
|
||||
const resultImage = await downloadImage(state.result![0]);
|
||||
const fileId = await uploadPhoto(ctx, resultImage);
|
||||
const media = InputMediaBuilder.photo(fileId, {
|
||||
caption: `Промпт: ${prompt}`
|
||||
});
|
||||
activeTasks.delete(userId);
|
||||
return ctx.api.editMessageMediaInline(messageId, media);
|
||||
} else if (state.status === 'ERROR') {
|
||||
activeTasks.delete(userId);
|
||||
logger.error('ERROR: failed to generate image');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createBot(token: string) {
|
||||
const bot = new Bot(token);
|
||||
bot.api.config.use(apiThrottler());
|
||||
const protectedBot = bot.errorBoundary((err) => logger.error(err));
|
||||
protectedBot.on('chosen_inline_result', handleChosenInlineResult);
|
||||
protectedBot.on('inline_query', handleInlineQuery);
|
||||
return bot;
|
||||
}
|
16
src/logger.ts
Normal file
16
src/logger.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { isDev } from "#root/utils.js";
|
||||
import { LoggerOptions, pino } from "pino";
|
||||
import PinoPretty, { PrettyOptions } from "pino-pretty";
|
||||
|
||||
const options: LoggerOptions = {
|
||||
level: isDev() ? 'trace' : 'info'
|
||||
};
|
||||
|
||||
const prettyOptions: PrettyOptions = {
|
||||
ignore: 'pid,hostname',
|
||||
colorize: isDev() ? true : false,
|
||||
translateTime: 'SYS:dd.mm.yyyy, HH:MM:ss'
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
export let logger = pino(options, PinoPretty(prettyOptions));
|
68
src/predict.ts
Normal file
68
src/predict.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { fooocos } from "#fooocos/client.js";
|
||||
import { components } from "#fooocos/schema.js";
|
||||
import { textToImageBody } from "#root/body.js";
|
||||
import { logger } from "#root/logger.js";
|
||||
import { sleep } from "#root/utils.js";
|
||||
|
||||
type Text2ImgRequest = components["schemas"]["Text2ImgRequest"];
|
||||
type AsyncJobResponse = components["schemas"]["AsyncJobResponse"];
|
||||
|
||||
type PredictStatus = "WAITING" | "RUNNING" | "SUCCESS" | "ERROR";
|
||||
|
||||
type PredictResponse = {
|
||||
status: PredictStatus,
|
||||
jobId: number,
|
||||
preview?: string,
|
||||
result?: string[];
|
||||
};
|
||||
|
||||
export async function* predict(prompt: string): AsyncGenerator<PredictResponse> {
|
||||
const defaultBody = textToImageBody(prompt) as Text2ImgRequest;
|
||||
const response = await fooocos.POST('/v1/generation/text-to-image', {
|
||||
body: defaultBody
|
||||
});
|
||||
|
||||
if (response.error) throw response.error;
|
||||
|
||||
const data = response.data as AsyncJobResponse;
|
||||
|
||||
while (true) {
|
||||
const poll = await fooocos.GET('/v1/generation/query-job', {
|
||||
params: {
|
||||
query: {
|
||||
job_id: data.job_id,
|
||||
require_step_preivew: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (poll.error) throw poll.error;
|
||||
|
||||
const status = poll.data.job_stage;
|
||||
|
||||
if (status === "WAITING") {
|
||||
logger.trace(`job id ${poll.data.job_id} is waiting`);
|
||||
yield { status: 'WAITING', jobId: poll.data.job_id };
|
||||
} else if (status === "RUNNING") {
|
||||
logger.trace(`job id ${poll.data.job_id} is running`);
|
||||
const stepPreview = poll.data.job_step_preview!;
|
||||
yield { status: 'RUNNING', jobId: poll.data.job_id, preview: stepPreview };
|
||||
} else if (status === 'SUCCESS') {
|
||||
logger.trace(`job id ${poll.data.job_id} is success`);
|
||||
const imageUrls: string[] = [];
|
||||
if (poll.data.job_result) {
|
||||
for (let image of poll.data.job_result) {
|
||||
imageUrls.push(image.url!.replace('http://127.0.0.1:8888', process.env["FOOOCOS_API_URL"]!));
|
||||
}
|
||||
}
|
||||
yield { status: 'SUCCESS', jobId: poll.data.job_id, result: imageUrls };
|
||||
break;
|
||||
} else if (status === 'ERROR') {
|
||||
logger.trace(`job id ${poll.data.job_id} is error`);
|
||||
yield { status: 'ERROR', jobId: poll.data.job_id };
|
||||
break;
|
||||
}
|
||||
|
||||
await sleep(3000);
|
||||
}
|
||||
}
|
30
src/utils.ts
Normal file
30
src/utils.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { fooocos } from "#fooocos/client.js";
|
||||
import { Context, InputFile } from "grammy";
|
||||
|
||||
export const isDev = () => process.env["NODE_ENV"] === 'development' ? true : false;
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export async function isFooocosAlive() {
|
||||
try {
|
||||
await fooocos.GET('/', { parseAs: 'text', signal: AbortSignal.timeout(1000) });
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadPhoto(ctx: Context, photo: Buffer) {
|
||||
const dummyMessage = await ctx.api.sendPhoto(process.env["DUMMY_CHAT_ID"]!, new InputFile(photo));
|
||||
const fileId = dummyMessage.photo[dummyMessage.photo.length - 1].file_id;
|
||||
return fileId;
|
||||
}
|
||||
|
||||
export async function downloadImage(url: string) {
|
||||
const response = await fetch(url)
|
||||
.then(r => r.arrayBuffer());
|
||||
const buffer = Buffer.from(response);
|
||||
return buffer;
|
||||
}
|
BIN
static/gorilla.gif
Normal file
BIN
static/gorilla.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.6 MiB |
BIN
static/output.png
Normal file
BIN
static/output.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"preserveWatchOutput": true,
|
||||
"noEmit": true,
|
||||
"module": "NodeNext",
|
||||
"target": "ES2021",
|
||||
"moduleResolution": "NodeNext",
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
"paths": {
|
||||
"#root/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"#fooocos/*": [
|
||||
"./fooocos/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user