Compare commits
3 Commits
047608e92a
...
7541deb178
Author | SHA1 | Date |
---|---|---|
pinks | 7541deb178 | |
pinks | 80bcd539bd | |
pinks | c3e5a4d908 |
31
README.md
31
README.md
|
@ -22,34 +22,3 @@ You can put these in `.env` file or pass them as environment variables.
|
||||||
|
|
||||||
- Start stable diffusion webui: `cd sd-webui`, `./webui.sh --api`
|
- Start stable diffusion webui: `cd sd-webui`, `./webui.sh --api`
|
||||||
- Start bot: `deno task start`
|
- Start bot: `deno task start`
|
||||||
|
|
||||||
## TODO
|
|
||||||
|
|
||||||
- [x] Keep generation history
|
|
||||||
- [x] Changing params, parsing png info in request
|
|
||||||
- [x] Cancelling jobs by deleting message
|
|
||||||
- [x] Multiple parallel workers
|
|
||||||
- [x] Replying to another text message to copy prompt and generate
|
|
||||||
- [x] Replying to bot message, conversation in DMs
|
|
||||||
- [x] Replying to png message to extract png info nad generate
|
|
||||||
- [ ] Banning tags
|
|
||||||
- [x] Img2Img + Upscale
|
|
||||||
- [ ] special param "scale" to change image size preserving aspect ratio
|
|
||||||
- [ ] Admin WebUI
|
|
||||||
- [ ] User daily generation limits
|
|
||||||
- [ ] Querying all generation history, displaying stats
|
|
||||||
- [ ] Analyzing prompt quality based on tag csv
|
|
||||||
- [ ] Report aliased/unknown tags based on csv
|
|
||||||
- [ ] Report unknown loras
|
|
||||||
- [ ] Investigate "sendMediaGroup failed"
|
|
||||||
- [ ] Changing sampler without error on unknown sampler
|
|
||||||
- [ ] Changing model
|
|
||||||
- [ ] Inpaint using telegram photo edit
|
|
||||||
- [ ] Outpaint
|
|
||||||
- [ ] Non-SD (extras) upscale
|
|
||||||
- [ ] Tiled generation to allow very big images
|
|
||||||
- [ ] Downloading raw images
|
|
||||||
- [ ] Extra prompt syntax, fixing `()+++` syntax
|
|
||||||
- [ ] Translations
|
|
||||||
- replace fmtDuration usage
|
|
||||||
- replace formatOrdinal usage
|
|
||||||
|
|
|
@ -115,8 +115,7 @@ async function img2img(
|
||||||
from: ctx.message.from,
|
from: ctx.message.from,
|
||||||
chat: ctx.message.chat,
|
chat: ctx.message.chat,
|
||||||
requestMessageId: ctx.message.message_id,
|
requestMessageId: ctx.message.message_id,
|
||||||
replyMessageId: replyMessage.message_id,
|
status: { type: "waiting", message: replyMessage },
|
||||||
status: { type: "waiting" },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger().debug(`Job enqueued for ${formatUserChat(ctx.message)}`);
|
logger().debug(`Job enqueued for ${formatUserChat(ctx.message)}`);
|
||||||
|
|
|
@ -16,6 +16,7 @@ export async function queueCommand(ctx: Grammy.CommandContext<Context>) {
|
||||||
.then((jobs) => jobs.map((job, index) => ({ ...job.value, place: index + 1 })));
|
.then((jobs) => jobs.map((job, index) => ({ ...job.value, place: index + 1 })));
|
||||||
const jobs = [...processingJobs, ...waitingJobs];
|
const jobs = [...processingJobs, ...waitingJobs];
|
||||||
const { bold } = GrammyParseMode;
|
const { bold } = GrammyParseMode;
|
||||||
|
|
||||||
return fmt([
|
return fmt([
|
||||||
"Current queue:\n",
|
"Current queue:\n",
|
||||||
...jobs.length > 0
|
...jobs.length > 0
|
||||||
|
@ -47,8 +48,9 @@ export async function queueCommand(ctx: Grammy.CommandContext<Context>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFutureUpdates() {
|
async function handleFutureUpdates() {
|
||||||
for (let idx = 0; idx < 20; idx++) {
|
for (let idx = 0; idx < 30; idx++) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
await ctx.api.sendChatAction(ctx.chat.id, "typing");
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 4000));
|
||||||
const nextFormattedMessage = await getMessageText();
|
const nextFormattedMessage = await getMessageText();
|
||||||
if (nextFormattedMessage.text !== formattedMessage.text) {
|
if (nextFormattedMessage.text !== formattedMessage.text) {
|
||||||
await ctx.api.editMessageText(
|
await ctx.api.editMessageText(
|
||||||
|
|
|
@ -96,8 +96,7 @@ async function txt2img(ctx: Context, match: string, includeRepliedTo: boolean):
|
||||||
from: ctx.message.from,
|
from: ctx.message.from,
|
||||||
chat: ctx.message.chat,
|
chat: ctx.message.chat,
|
||||||
requestMessageId: ctx.message.message_id,
|
requestMessageId: ctx.message.message_id,
|
||||||
replyMessageId: replyMessage.message_id,
|
status: { type: "waiting", message: replyMessage },
|
||||||
status: { type: "waiting" },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger().debug(`Job enqueued for ${formatUserChat(ctx.message)}`);
|
logger().debug(`Job enqueued for ${formatUserChat(ctx.message)}`);
|
||||||
|
|
|
@ -4,16 +4,36 @@ import { db } from "./db.ts";
|
||||||
|
|
||||||
export interface JobSchema {
|
export interface JobSchema {
|
||||||
task:
|
task:
|
||||||
| { type: "txt2img"; params: Partial<PngInfo> }
|
| {
|
||||||
| { type: "img2img"; params: Partial<PngInfo>; fileId: string };
|
type: "txt2img";
|
||||||
|
params: Partial<PngInfo>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "img2img";
|
||||||
|
params: Partial<PngInfo>;
|
||||||
|
fileId: string;
|
||||||
|
};
|
||||||
from: GrammyTypes.User;
|
from: GrammyTypes.User;
|
||||||
chat: GrammyTypes.Chat;
|
chat: GrammyTypes.Chat;
|
||||||
requestMessageId: number;
|
requestMessageId: number;
|
||||||
replyMessageId?: number;
|
|
||||||
status:
|
status:
|
||||||
| { type: "waiting" }
|
| {
|
||||||
| { type: "processing"; progress: number; worker: string; updatedDate: Date }
|
type: "waiting";
|
||||||
| { type: "done"; info?: SdTxt2ImgInfo; startDate?: Date; endDate?: Date };
|
message?: GrammyTypes.Message.TextMessage;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "processing";
|
||||||
|
progress: number;
|
||||||
|
worker: string;
|
||||||
|
updatedDate: Date;
|
||||||
|
message?: GrammyTypes.Message.TextMessage;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "done";
|
||||||
|
info?: SdTxt2ImgInfo;
|
||||||
|
startDate?: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const jobStore = new IKV.Store(db, "job", {
|
export const jobStore = new IKV.Store(db, "job", {
|
||||||
|
|
|
@ -40,10 +40,16 @@ export async function processJobs(): Promise<never> {
|
||||||
if (!worker) continue;
|
if (!worker) continue;
|
||||||
|
|
||||||
// process the job
|
// process the job
|
||||||
await job.update({
|
await job.update((value) => ({
|
||||||
status: { type: "processing", progress: 0, worker: worker.name, updatedDate: new Date() },
|
...value,
|
||||||
});
|
status: {
|
||||||
|
type: "processing",
|
||||||
|
progress: 0,
|
||||||
|
worker: worker.name,
|
||||||
|
updatedDate: new Date(),
|
||||||
|
message: job.value.status.type !== "done" ? job.value.status.message : undefined,
|
||||||
|
},
|
||||||
|
}));
|
||||||
busyWorkers.add(worker.name);
|
busyWorkers.add(worker.name);
|
||||||
processJob(job, worker, config)
|
processJob(job, worker, config)
|
||||||
.catch(async (err) => {
|
.catch(async (err) => {
|
||||||
|
@ -89,31 +95,63 @@ async function processJob(job: IKV.Model<JobSchema>, worker: WorkerData, config:
|
||||||
);
|
);
|
||||||
const startDate = new Date();
|
const startDate = new Date();
|
||||||
|
|
||||||
// if there is already a status message delete it
|
// if there is already a status message and its older than 10 seconds
|
||||||
if (job.value.replyMessageId) {
|
if (
|
||||||
await bot.api.deleteMessage(job.value.chat.id, job.value.replyMessageId)
|
job.value.status.type === "processing" && job.value.status.message &&
|
||||||
.catch(() => undefined);
|
(Date.now() - job.value.status.message.date * 1000) > 10 * 1000
|
||||||
|
) {
|
||||||
|
// delete it
|
||||||
|
await bot.api.deleteMessage(
|
||||||
|
job.value.status.message.chat.id,
|
||||||
|
job.value.status.message.message_id,
|
||||||
|
).catch(() => undefined);
|
||||||
|
await job.update((value) => ({
|
||||||
|
...value,
|
||||||
|
status: { ...value.status, message: undefined },
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// send a new status message
|
// we have to check if job is still processing at every step because TypeScript
|
||||||
const newStatusMessage = await bot.api.sendMessage(
|
if (job.value.status.type === "processing") {
|
||||||
job.value.chat.id,
|
await bot.api.sendChatAction(job.value.chat.id, "upload_photo", { maxAttempts: 1 })
|
||||||
`Generating your prompt now... 0% using ${worker.name}`,
|
.catch(() => undefined);
|
||||||
{ reply_to_message_id: job.value.requestMessageId },
|
// if now there is no status message
|
||||||
).catch((err) => {
|
if (!job.value.status.message) {
|
||||||
// don't error if the request message was deleted
|
// send a new status message
|
||||||
if (err instanceof Grammy.GrammyError && err.message.match(/repl(y|ied)/)) return null;
|
const statusMessage = await bot.api.sendMessage(
|
||||||
else throw err;
|
job.value.chat.id,
|
||||||
});
|
`Generating your prompt now... 0% using ${worker.name}`,
|
||||||
// if the request message was deleted, cancel the job
|
{ reply_to_message_id: job.value.requestMessageId },
|
||||||
if (!newStatusMessage) {
|
).catch((err) => {
|
||||||
|
// if the request message (the message we are replying to) was deleted
|
||||||
|
if (err instanceof Grammy.GrammyError && err.message.match(/repl(y|ied)/)) {
|
||||||
|
// jest set the status message to undefined
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
await job.update((value) => ({
|
||||||
|
...value,
|
||||||
|
status: { ...value.status, message: statusMessage },
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// edit the existing status message
|
||||||
|
await bot.api.editMessageText(
|
||||||
|
job.value.status.message.chat.id,
|
||||||
|
job.value.status.message.message_id,
|
||||||
|
`Generating your prompt now... 0% using ${worker.name}`,
|
||||||
|
{ maxAttempts: 1 },
|
||||||
|
).catch(() => undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we don't have a status message (it failed sending because request was deleted)
|
||||||
|
if (job.value.status.type === "processing" && !job.value.status.message) {
|
||||||
|
// cancel the job
|
||||||
await job.delete();
|
await job.delete();
|
||||||
logger().info(
|
logger().info(`Job cancelled for ${formatUserChat(job.value)}`);
|
||||||
`Job cancelled for ${formatUserChat(job.value)}`,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await job.update({ replyMessageId: newStatusMessage.message_id });
|
|
||||||
|
|
||||||
// reduce size if worker can't handle the resolution
|
// reduce size if worker can't handle the resolution
|
||||||
const size = limitSize(
|
const size = limitSize(
|
||||||
|
@ -123,25 +161,29 @@ async function processJob(job: IKV.Model<JobSchema>, worker: WorkerData, config:
|
||||||
|
|
||||||
// process the job
|
// process the job
|
||||||
const handleProgress = async (progress: SdProgressResponse) => {
|
const handleProgress = async (progress: SdProgressResponse) => {
|
||||||
// important: don't let any errors escape this callback
|
if (job.value.status.type === "processing" && job.value.status.message) {
|
||||||
if (job.value.replyMessageId) {
|
await Promise.all([
|
||||||
await bot.api.editMessageText(
|
bot.api.sendChatAction(job.value.chat.id, "upload_photo", { maxAttempts: 1 }),
|
||||||
job.value.chat.id,
|
bot.api.editMessageText(
|
||||||
job.value.replyMessageId,
|
job.value.status.message.chat.id,
|
||||||
`Generating your prompt now... ${
|
job.value.status.message.message_id,
|
||||||
(progress.progress * 100).toFixed(0)
|
`Generating your prompt now... ${
|
||||||
}% using ${worker.name}`,
|
(progress.progress * 100).toFixed(0)
|
||||||
{ maxAttempts: 1 },
|
}% using ${worker.name}`,
|
||||||
).catch(() => undefined);
|
{ maxAttempts: 1 },
|
||||||
|
),
|
||||||
|
job.update((value) => ({
|
||||||
|
...value,
|
||||||
|
status: {
|
||||||
|
type: "processing",
|
||||||
|
progress: progress.progress,
|
||||||
|
worker: worker.name,
|
||||||
|
updatedDate: new Date(),
|
||||||
|
message: value.status.type !== "done" ? value.status.message : undefined,
|
||||||
|
},
|
||||||
|
}), { maxAttempts: 1 }),
|
||||||
|
]).catch(() => undefined);
|
||||||
}
|
}
|
||||||
await job.update({
|
|
||||||
status: {
|
|
||||||
type: "processing",
|
|
||||||
progress: progress.progress,
|
|
||||||
worker: worker.name,
|
|
||||||
updatedDate: new Date(),
|
|
||||||
},
|
|
||||||
}, { maxAttempts: 1 }).catch(() => undefined);
|
|
||||||
};
|
};
|
||||||
let response: SdResponse<unknown>;
|
let response: SdResponse<unknown>;
|
||||||
const taskType = job.value.task.type; // don't narrow this to never pls typescript
|
const taskType = job.value.task.type; // don't narrow this to never pls typescript
|
||||||
|
@ -169,11 +211,11 @@ async function processJob(job: IKV.Model<JobSchema>, worker: WorkerData, config:
|
||||||
throw new Error(`Unknown task type: ${taskType}`);
|
throw new Error(`Unknown task type: ${taskType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload the result
|
// change status message to uploading images
|
||||||
if (job.value.replyMessageId) {
|
if (job.value.status.type === "processing" && job.value.status.message) {
|
||||||
await bot.api.editMessageText(
|
await bot.api.editMessageText(
|
||||||
job.value.chat.id,
|
job.value.status.message.chat.id,
|
||||||
job.value.replyMessageId,
|
job.value.status.message.message_id,
|
||||||
`Uploading your images...`,
|
`Uploading your images...`,
|
||||||
).catch(() => undefined);
|
).catch(() => undefined);
|
||||||
}
|
}
|
||||||
|
@ -201,10 +243,13 @@ async function processJob(job: IKV.Model<JobSchema>, worker: WorkerData, config:
|
||||||
: [],
|
: [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// sending images loop because telegram is unreliable and it would be a shame to lose the images
|
||||||
let sendMediaAttempt = 0;
|
let sendMediaAttempt = 0;
|
||||||
let resultMessages: GrammyTypes.Message.MediaMessage[] | undefined;
|
let resultMessages: GrammyTypes.Message.MediaMessage[] | undefined;
|
||||||
while (true) {
|
while (true) {
|
||||||
sendMediaAttempt++;
|
sendMediaAttempt++;
|
||||||
|
await bot.api.sendChatAction(job.value.chat.id, "upload_photo", { maxAttempts: 1 })
|
||||||
|
.catch(() => undefined);
|
||||||
|
|
||||||
// parse files from reply JSON
|
// parse files from reply JSON
|
||||||
const inputFiles = await Promise.all(
|
const inputFiles = await Promise.all(
|
||||||
|
@ -232,7 +277,12 @@ async function processJob(job: IKV.Model<JobSchema>, worker: WorkerData, config:
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger().warning(`Sending images (attempt ${sendMediaAttempt}) failed: ${err}`);
|
logger().warning(`Sending images (attempt ${sendMediaAttempt}) failed: ${err}`);
|
||||||
if (sendMediaAttempt >= 6) throw err;
|
if (sendMediaAttempt >= 6) throw err;
|
||||||
await Async.delay(10000);
|
// wait 2 * 5 seconds before retrying
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
await bot.api.sendChatAction(job.value.chat.id, "upload_photo", { maxAttempts: 1 })
|
||||||
|
.catch(() => undefined);
|
||||||
|
await Async.delay(5000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,17 +295,23 @@ async function processJob(job: IKV.Model<JobSchema>, worker: WorkerData, config:
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete the status message
|
// delete the status message
|
||||||
if (job.value.replyMessageId) {
|
if (job.value.status.type === "processing" && job.value.status.message) {
|
||||||
await bot.api.deleteMessage(job.value.chat.id, job.value.replyMessageId)
|
await bot.api.deleteMessage(
|
||||||
.catch(() => undefined)
|
job.value.status.message.chat.id,
|
||||||
.then(() => job.update({ replyMessageId: undefined }))
|
job.value.status.message.message_id,
|
||||||
.catch(() => undefined);
|
).catch(() => undefined);
|
||||||
|
await job.update((value) => ({
|
||||||
|
...value,
|
||||||
|
status: { ...value.status, message: undefined },
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// update job to status done
|
// update job to status done
|
||||||
await job.update({
|
await job.update((value) => ({
|
||||||
|
...value,
|
||||||
status: { type: "done", info: response.info, startDate, endDate: new Date() },
|
status: { type: "done", info: response.info, startDate, endDate: new Date() },
|
||||||
});
|
}));
|
||||||
|
|
||||||
logger().debug(
|
logger().debug(
|
||||||
`Job finished for ${formatUserChat(job.value)} using ${worker.name}${
|
`Job finished for ${formatUserChat(job.value)} using ${worker.name}${
|
||||||
sendMediaAttempt > 1 ? ` after ${sendMediaAttempt} attempts` : ""
|
sendMediaAttempt > 1 ? ` after ${sendMediaAttempt} attempts` : ""
|
||||||
|
|
|
@ -14,10 +14,10 @@ export async function updateJobStatusMsgs(): Promise<never> {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||||
const jobs = await jobStore.getBy("status.type", "waiting");
|
const jobs = await jobStore.getBy("status.type", "waiting");
|
||||||
for (const [index, job] of jobs.entries()) {
|
for (const [index, job] of jobs.entries()) {
|
||||||
if (!job.value.replyMessageId) continue;
|
if (job.value.status.type !== "waiting" || !job.value.status.message) continue;
|
||||||
await bot.api.editMessageText(
|
await bot.api.editMessageText(
|
||||||
job.value.chat.id,
|
job.value.status.message.chat.id,
|
||||||
job.value.replyMessageId,
|
job.value.status.message.message_id,
|
||||||
`You are ${formatOrdinal(index + 1)} in queue.`,
|
`You are ${formatOrdinal(index + 1)} in queue.`,
|
||||||
{ maxAttempts: 1 },
|
{ maxAttempts: 1 },
|
||||||
).catch(() => undefined);
|
).catch(() => undefined);
|
||||||
|
|
Loading…
Reference in New Issue