Skip to content

Commit dcb8e81

Browse files
committed
feat: support variation
1 parent a57d8bc commit dcb8e81

File tree

6 files changed

+161
-36
lines changed

6 files changed

+161
-36
lines changed

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ const image = await midjourney.imagine('your prompt')
3434

3535
const data = await midjourney.upscale('your prompt', {
3636
messageId: image.id,
37-
index: 1,
3837
// custom_id could be found at image.component, for example: MJ::JOB::upsample::1::0c266431-26c6-47fa-bfee-2e1e11c7a66f
3938
customId: 'component custom_id'
4039
})
@@ -43,6 +42,28 @@ const data = await midjourney.upscale('your prompt', {
4342
console.log(data.attachments[0].url)
4443
```
4544

45+
### variation
46+
```typescript
47+
import { Midjourney } from 'midjourney-fetch'
48+
49+
const midjourney = new Midjourney({
50+
channelId: 'your channelId',
51+
serverId: 'your serverId',
52+
token: 'your token',
53+
})
54+
55+
const image = await midjourney.imagine('your prompt')
56+
57+
const data = await midjourney.variation('your prompt', {
58+
messageId: image.id,
59+
// custom_id could be found at image.component, for example: MJ::JOB::variation::1::0c266431-26c6-47fa-bfee-2e1e11c7a66f
60+
customId: 'component custom_id'
61+
})
62+
63+
// generated image url
64+
console.log(data.attachments[0].url)
65+
```
66+
4667
## How to get Ids and Token
4768
- [How to find ids](https://docs.statbot.net/docs/faq/general/how-find-id/)
4869
- [Get discord token](https://www.androidauthority.com/get-discord-token-3149920/)

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "midjourney-fetch",
3-
"version": "1.0.0-beta.0",
3+
"version": "1.0.0",
44
"description": "",
55
"type": "module",
66
"main": "./dist/index.js",
@@ -57,6 +57,7 @@
5757
]
5858
},
5959
"dependencies": {
60-
"@sapphire/snowflake": "^3.5.1"
60+
"@sapphire/snowflake": "^3.5.1",
61+
"dayjs": "^1.11.7"
6162
}
6263
}

pnpm-lock.yaml

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/interface.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,33 @@ export interface MessageItem {
4343
components: MessageComponent[];
4444
type: number;
4545
}>;
46+
message_reference?: {
47+
channel_id: string;
48+
guild_id: string;
49+
message_id: string;
50+
};
51+
timestamp?: string;
4652
}
4753

48-
export type MessageType = 'imagine' | 'upscale';
54+
export type MessageType = 'imagine' | 'upscale' | 'variation';
4955

50-
export type MessageTypeProps =
56+
export type MessageTypeProps = {
57+
timestamp: string;
58+
} & (
59+
| {
60+
type: Extract<MessageType, 'variation'>;
61+
index: number;
62+
}
5163
| {
5264
type: Extract<MessageType, 'upscale'>;
5365
index: number;
5466
}
5567
| {
5668
type?: Extract<MessageType, 'imagine'>;
57-
};
69+
}
70+
);
5871

5972
export interface UpscaleProps {
6073
messageId: string;
61-
index: number;
62-
hash?: string;
63-
customId?: string;
74+
customId: string;
6475
}

src/midjourney.ts

Lines changed: 78 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@ import { DiscordSnowflake } from '@sapphire/snowflake';
22
import { configs, defaultSessionId, midjourneyBotConfigs } from './config';
33
import type {
44
MessageItem,
5+
MessageType,
56
MessageTypeProps,
67
MidjourneyProps,
78
UpscaleProps,
89
} from './interface';
9-
import { findMessageByPrompt, isInProgress } from './utils';
10+
import {
11+
findMessageByPrompt,
12+
getHashFromCustomId,
13+
isInProgress,
14+
} from './utils';
1015

1116
export class Midjourney {
1217
protected readonly channelId: string;
@@ -45,6 +50,17 @@ export class Midjourney {
4550
}
4651
}
4752

53+
async interactions(payload: any) {
54+
return fetch(`https://discord.com/api/v9/interactions`, {
55+
method: 'POST',
56+
body: JSON.stringify(payload),
57+
headers: {
58+
'Content-Type': 'application/json',
59+
Authorization: this.token,
60+
},
61+
});
62+
}
63+
4864
async createImage(prompt: string) {
4965
const payload = {
5066
type: 2,
@@ -90,14 +106,7 @@ export class Midjourney {
90106
nonce: DiscordSnowflake.generate().toString(),
91107
};
92108

93-
const res = await fetch(`https://discord.com/api/v9/interactions`, {
94-
method: 'POST',
95-
body: JSON.stringify(payload),
96-
headers: {
97-
'Content-Type': 'application/json',
98-
Authorization: this.token,
99-
},
100-
});
109+
const res = await this.interactions(payload);
101110
if (res.status >= 400) {
102111
let message = '';
103112
try {
@@ -113,7 +122,10 @@ export class Midjourney {
113122
}
114123
}
115124

116-
async createUpscale({ messageId, index, hash, customId }: UpscaleProps) {
125+
async createUpscaleOrVariation(
126+
type: Exclude<MessageType, 'imagine'>,
127+
{ messageId, customId }: UpscaleProps
128+
) {
117129
const payload = {
118130
type: 3,
119131
nonce: DiscordSnowflake.generate().toString(),
@@ -125,33 +137,26 @@ export class Midjourney {
125137
session_id: defaultSessionId,
126138
data: {
127139
component_type: 2,
128-
custom_id: customId || `MJ::JOB::upsample::${index}::${hash}`,
140+
custom_id: customId,
129141
},
130142
};
131-
const res = await fetch(`https://discord.com/api/v9/interactions`, {
132-
method: 'POST',
133-
body: JSON.stringify(payload),
134-
headers: {
135-
'Content-Type': 'application/json',
136-
Authorization: this.token,
137-
},
138-
});
143+
const res = await this.interactions(payload);
139144
if (res.status >= 400) {
140145
let message = '';
141146
try {
142147
const data = await res.json();
143148
if (this.debugger) {
144-
this.log('Create upscale failed', JSON.stringify(data));
149+
this.log(`Create ${type} failed`, JSON.stringify(data));
145150
}
146151
message = data?.message;
147152
} catch (e) {
148153
// catch JSON error
149154
}
150-
throw new Error(message || `Create upscale failed with ${res.status}`);
155+
throw new Error(message || `Create ${type} failed with ${res.status}`);
151156
}
152157
}
153158

154-
async getMessage(prompt: string, options?: MessageTypeProps) {
159+
async getMessage(prompt: string, options: MessageTypeProps) {
155160
const res = await fetch(
156161
`https://discord.com/api/v10/channels/${this.channelId}/messages?limit=50`,
157162
{
@@ -170,7 +175,10 @@ export class Midjourney {
170175
* Same with /imagine command
171176
*/
172177
async imagine(prompt: string) {
178+
const timestamp = new Date().toISOString();
179+
173180
await this.createImage(prompt);
181+
174182
const times = this.timeout / this.interval;
175183
let count = 0;
176184
let result: MessageItem | undefined;
@@ -179,7 +187,7 @@ export class Midjourney {
179187
count += 1;
180188
await new Promise((res) => setTimeout(res, this.interval));
181189
this.log(count, 'imagine');
182-
const message = await this.getMessage(prompt);
190+
const message = await this.getMessage(prompt, { timestamp });
183191
if (message && !isInProgress(message)) {
184192
result = message;
185193
break;
@@ -192,18 +200,63 @@ export class Midjourney {
192200
}
193201

194202
async upscale({ prompt, ...params }: UpscaleProps & { prompt: string }) {
195-
await this.createUpscale(params);
203+
const { index } = getHashFromCustomId('upscale', params.customId);
196204
const times = this.timeout / this.interval;
197205
let count = 0;
198206
let result: MessageItem | undefined;
207+
208+
if (!index) {
209+
throw new Error('Create upscale failed with 400, unknown customId');
210+
}
211+
212+
const timestamp = new Date().toISOString();
213+
214+
await this.createUpscaleOrVariation('upscale', params);
215+
199216
while (count < times) {
200217
try {
201218
count += 1;
202219
await new Promise((res) => setTimeout(res, this.interval));
203220
this.log(count, 'upscale');
204221
const message = await this.getMessage(prompt, {
205222
type: 'upscale',
206-
index: params.index,
223+
index,
224+
timestamp,
225+
});
226+
if (message && !isInProgress(message)) {
227+
result = message;
228+
break;
229+
}
230+
} catch {
231+
continue;
232+
}
233+
}
234+
return result;
235+
}
236+
237+
async variation({ prompt, ...params }: UpscaleProps & { prompt: string }) {
238+
const { index } = getHashFromCustomId('variation', params.customId);
239+
const times = this.timeout / this.interval;
240+
let count = 0;
241+
let result: MessageItem | undefined;
242+
243+
if (!index) {
244+
throw new Error('Create variation failed with 400, unknown customId');
245+
}
246+
247+
const timestamp = new Date().toISOString();
248+
249+
await this.createUpscaleOrVariation('variation', params);
250+
251+
while (count < times) {
252+
try {
253+
count += 1;
254+
await new Promise((res) => setTimeout(res, this.interval));
255+
this.log(count, 'variation');
256+
const message = await this.getMessage(prompt, {
257+
type: 'variation',
258+
index,
259+
timestamp,
207260
});
208261
if (message && !isInProgress(message)) {
209262
result = message;

src/utils.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,41 @@
1+
import dayjs from 'dayjs';
12
import { midjourneyBotConfigs } from './config';
2-
import type { MessageTypeProps, MessageItem } from './interface';
3+
import type { MessageTypeProps, MessageItem, MessageType } from './interface';
34

45
export const findMessageByPrompt = (
56
messages: MessageItem[],
67
prompt: string,
7-
options?: MessageTypeProps
8+
options: MessageTypeProps
89
) => {
910
// trim and merge spaces
1011
const filterPrompt = prompt.split(' ').filter(Boolean).join(' ');
1112
if (options?.type === 'upscale') {
1213
return messages.find(
1314
(msg) =>
15+
msg.timestamp &&
16+
dayjs(msg.timestamp).isAfter(options.timestamp) &&
1417
msg.type === 19 &&
1518
msg.content.includes(filterPrompt) &&
1619
msg.content.includes(`Image #${options.index}`) &&
1720
msg.author.id === midjourneyBotConfigs.applicationId
1821
);
1922
}
23+
if (options?.type === 'variation') {
24+
return messages.find(
25+
(msg) =>
26+
msg.timestamp &&
27+
dayjs(msg.timestamp).isAfter(options.timestamp) &&
28+
msg.type === 19 &&
29+
msg.content.includes(filterPrompt) &&
30+
// 0 means reroll
31+
(!options?.index || msg.content.includes('Variations')) &&
32+
msg.author.id === midjourneyBotConfigs.applicationId
33+
);
34+
}
2035
return messages.find(
2136
(msg) =>
37+
msg.timestamp &&
38+
dayjs(msg.timestamp).isAfter(options.timestamp) &&
2239
msg.content.includes(filterPrompt) &&
2340
msg.author.id === midjourneyBotConfigs.applicationId
2441
);
@@ -28,3 +45,18 @@ export const isInProgress = (message: MessageItem) =>
2845
message.attachments.length === 0 ||
2946
(message.attachments[0]?.filename?.startsWith('grid') &&
3047
message.attachments[0]?.filename?.endsWith('.webp'));
48+
49+
export const getHashFromCustomId = (type: MessageType, id: string) => {
50+
let regex: RegExp | null = null;
51+
if (type === 'upscale') {
52+
regex = /(upsample)::(\d+)::(.+)/;
53+
} else if (type === 'variation') {
54+
regex = /(variation|reroll)::(\d+)::(.+)/;
55+
}
56+
if (!regex) return { index: null, hash: null };
57+
const match = id.match(regex);
58+
const model = match?.[1]; // upsample|variation|reroll
59+
const index = match?.[2] ? Number(match[2]) : null;
60+
const hash = match?.[3] || null;
61+
return { model, index, hash };
62+
};

0 commit comments

Comments
 (0)