From 2c7824ebc8abb771063d5fbd7ecbda22700e537d Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Thu, 12 Feb 2026 18:21:29 +0300 Subject: [PATCH] (feat): add image generation from openai --- src/commands/openai-gen-image.ts | 120 +++++++++++++++++++++++++++++++ src/common/environment.ts | 2 + src/index.ts | 2 + 3 files changed, 124 insertions(+) create mode 100644 src/commands/openai-gen-image.ts diff --git a/src/commands/openai-gen-image.ts b/src/commands/openai-gen-image.ts new file mode 100644 index 0000000..34e0164 --- /dev/null +++ b/src/commands/openai-gen-image.ts @@ -0,0 +1,120 @@ +import {ChatCommand} from "../base/chat-command"; +import {Message} from "typescript-telegram-bot-api"; +import {Requirements} from "../base/requirements"; +import {Requirement} from "../base/requirement"; +import {bot, openAi, photoDir} from "../index"; +import fs from "node:fs"; +import path from "node:path"; +import {editMessageText, logError, replyToMessage} from "../util/utils"; +import {Environment} from "../common/environment"; +import {APIError} from "openai"; + +export class OpenAIGenImage extends ChatCommand { + command = ["openAiGenImage", "chatGPTGenImage"]; + + title = "/openAIGenImage"; + description = "Generate image from OpenAI"; + + argsMode = "required" as const; + requirements = Requirements.Build(Requirement.BOT_CREATOR); + + async execute(msg: Message, match?: RegExpExecArray): Promise { + const prompt = match?.[3]?.trim(); + if (!prompt?.length) return; + + let waitMessage: Message | null = null; + + try { + const totalParts = 3; + const model = Environment.OPENAI_IMAGE_MODEL; + const size = "1024x1024"; + const fileFullName = `${msg.chat.id}_${msg.message_id}.png`; + const getFileLocation = (fn: string) => { + const genRoot = path.join(photoDir, "gen"); + if (!fs.existsSync(genRoot)) { + fs.mkdirSync(genRoot); + } + return path.join(genRoot, fn); + }; + + waitMessage = await replyToMessage({message: msg, text: "🌈 Генерирую изображение..."}); + + const stream = await openAi.images.generate({ + model: model, + prompt: prompt, + n: 1, + size: size, + stream: true, + partial_images: totalParts, + }); + + const then = Date.now(); + + for await (const event of stream) { + switch (event.type) { + case "image_generation.partial_image": { + console.log(` Partial image ${event.partial_image_index + 1}/3 received`); + console.log(` Size: ${event.b64_json.length} characters (base64)`); + + const fileName = `partial_${event.partial_image_index + 1}_${fileFullName}`; + const imageBuffer = Buffer.from(event.b64_json, "base64"); + const fileLocation = getFileLocation(fileName); + fs.writeFileSync(fileLocation, imageBuffer); + console.log(` 💾 Saved to: ${path.resolve(fileLocation)}`); + + await bot.editMessageMedia({ + chat_id: msg.chat.id, + message_id: waitMessage.message_id, + media: { + type: "photo", + media: imageBuffer, + caption: `🌈 Генерирую изображение (${(event.partial_image_index + 1)}/${totalParts})...` + } + }); + break; + } + case "image_generation.completed": { + console.log("\n✅ Final image completed!"); + console.log(` Size: ${event.b64_json.length} characters (base64)`); + + const imageBuffer = Buffer.from(event.b64_json, "base64"); + const fileLocation = getFileLocation(fileFullName); + fs.writeFileSync(fileLocation, imageBuffer); + console.log(` Saved to: ${path.resolve(fileLocation)}`); + + const diff = Date.now() - then; + await bot.editMessageMedia({ + chat_id: msg.chat.id, + message_id: waitMessage.message_id, + media: { + type: "photo", + media: imageBuffer, + caption: `🌈 Изображение по запросу "${prompt}" сгенерировано моделью "${model}" размеров ${size} за ${diff}ms` + } + }); + break; + } + default: + console.log(`❓ Unknown event: ${event}`); + } + } + } catch (e) { + logError(e); + + if (e instanceof APIError && e.error.code === "moderation_blocked") { + const text = "❌ Мне запрещено такое генерировать 😠"; + + if (waitMessage) { + await editMessageText(msg.chat.id, waitMessage.message_id, text).catch(logError); + } else { + await replyToMessage({message: msg, text: text}).catch(logError); + } + } else { + await replyToMessage({ + message: waitMessage ? waitMessage : msg, + text: `Произошла ошибка: ${e}` + }).catch(logError); + } + } + } +} \ No newline at end of file diff --git a/src/common/environment.ts b/src/common/environment.ts index 2ad9584..7860703 100644 --- a/src/common/environment.ts +++ b/src/common/environment.ts @@ -45,6 +45,7 @@ export class Environment { static OPENAI_BASE_URL?: string; static OPENAI_API_KEY?: string; static OPENAI_MODEL: string; + static OPENAI_IMAGE_MODEL: string; static waitText = "⏳ Дайте-ка подумать..."; static analyzingPictureText = "🔍 Внимательно изучаю изображение..."; @@ -93,6 +94,7 @@ export class Environment { Environment.OPENAI_BASE_URL = process.env.OPENAI_BASE_URL; Environment.OPENAI_API_KEY = process.env.OPENAI_API_KEY; Environment.OPENAI_MODEL = process.env.OPENAI_MODEL || "gpt-4.1-nano"; + Environment.OPENAI_IMAGE_MODEL = process.env.OPENAI_IMAGE_MODEL || "gpt-image-1-mini"; } static setAdmins(admins: Set) { diff --git a/src/index.ts b/src/index.ts index ff5fe5c..cb3f4ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -79,6 +79,7 @@ import {OpenAIListModels} from "./commands/openai-list-models"; import {OpenAIGetModel} from "./commands/openai-get-model"; import {OpenAISetModel} from "./commands/openai-set-model"; import {Info} from "./commands/info"; +import {OpenAIGenImage} from "./commands/openai-gen-image"; process.setUncaughtExceptionCaptureCallback(logError); @@ -210,6 +211,7 @@ if (Environment.OPENAI_API_KEY) { new OpenAIListModels(), new OpenAIGetModel(), new OpenAISetModel(), + new OpenAIGenImage() ); }