Initial commit

This commit is contained in:
Timofey Gelazoniya 2023-11-25 14:33:11 +03:00
commit a9613544b7
Signed by: zeldon
GPG Key ID: 047886915281DD2A
18 changed files with 5458 additions and 0 deletions

4
.env.sample Normal file
View File

@ -0,0 +1,4 @@
NODE_ENV=development
BOT_TOKEN=
FOOOCOS_API_URL=
DUMMY_CHAT_ID=

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.vscode
node_modules
.env

21
LICENSE Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

1121
fooocos/schema.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

16
index.ts Normal file
View 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

File diff suppressed because it is too large Load Diff

33
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

BIN
static/output.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

23
tsconfig.json Normal file
View 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/*"
]
}
}
}