@mymj/midjourney
Version:
Node.js client for the unofficial MidJourney API.
236 lines (211 loc) • 6.79 kB
text/typescript
import { Snowyflake, Epoch } from "snowyflake";
import { MJInfo, MJOptions, MJSubscribe } from "../interfaces";
export const sleep = async (ms: number): Promise<void> =>
await new Promise((resolve) => setTimeout(resolve, ms));
export const random = (min: number, max: number): number =>
Math.floor(Math.random() * (max - min) + min);
const snowflake = new Snowyflake({
workerId: 0n,
processId: 0n,
epoch: Epoch.Discord, // BigInt timestamp
});
export const nextNonce = (): string => snowflake.nextId().toString();
export const formatPrompts = (prompts: string) => {
const regex = /(\d️⃣ .+)/g;
const matches = prompts.match(regex);
if (matches) {
const shortenedPrompts = matches.map((match) => match.trim());
return shortenedPrompts;
} else {
return [];
}
};
export const formatOptions = (components: any) => {
var data: MJOptions[] = [];
for (var i = 0; i < components.length; i++) {
const component = components[i];
if (component.components && component.components.length > 0) {
const item = formatOptions(component.components);
data = data.concat(item);
}
if (!component.custom_id) continue;
data.push({
type: component.type,
style: component.style,
label: component.label || component.emoji?.name,
custom: component.custom_id,
});
}
return data;
};
export const componentsToHash = (components: any) => {
const options = formatOptions(components);
for (let i = 0; i < options.length; i++) {
const option = options[i];
const list = option.custom.split('::');
const hash = list.find(item => item.length === 36 && /^\w+(-\w+)+$/.test(item));
if (hash) {
return hash;
}
}
}
export const formatInfo = (msg: string) => {
let jsonResult: MJInfo = {
subscription: "",
jobMode: "",
visibilityMode: "",
fastTimeRemaining: "",
lifetimeUsage: "",
relaxedUsage: "",
queuedJobsFast: "",
queuedJobsRelax: "",
runningJobs: "",
message: "",
}; // Initialize jsonResult with empty object
msg.split("\n").forEach(function (line) {
const colonIndex = line.indexOf(":");
if (colonIndex > -1) {
const key = line.substring(0, colonIndex).trim().replaceAll("**", "");
const value = line.substring(colonIndex + 1).trim();
switch (key) {
case "Subscription":
jsonResult.subscription = value;
break;
case "Job Mode":
jsonResult.jobMode = value;
break;
case "Visibility Mode":
jsonResult.visibilityMode = value;
break;
case "Fast Time Remaining":
jsonResult.fastTimeRemaining = value;
break;
case "Lifetime Usage":
jsonResult.lifetimeUsage = value;
break;
case "Relaxed Usage":
jsonResult.relaxedUsage = value;
break;
case "Queued Jobs (fast)":
jsonResult.queuedJobsFast = value;
break;
case "Queued Jobs (relax)":
jsonResult.queuedJobsRelax = value;
break;
case "Running Jobs":
jsonResult.runningJobs = value;
break;
default:
// Do nothing
}
}
});
if (!jsonResult.subscription) {
jsonResult.message = msg;
}
return jsonResult;
};
export const formatSubscribe = (msg: MJSubscribe) => {
return msg;
};
// works for done png image
export const uriToHash = (uri: string) => {
return uri.split("_").pop()?.split(".")[0] ?? "";
};
// works for progress webp image
export const filenameToHash = (filename: string) => {
return filename.split("_")[0] ?? "";
};
export const content2progress = (content: string) => {
if (!content) return "";
const spcon = content.split("<@");
if (spcon.length < 2) {
return "";
}
content = spcon[1];
const regex = /\(([^)]+)\)/; // matches the value inside the first parenthesis
const match = content.match(regex);
let progress = "";
if (match) {
if (match[1].includes('fast') || match[1].includes('relax') || match[1].includes('turbo')) {
progress = 'done';
} else {
progress = match[1];
}
}
return progress;
};
// 求最大公约数
const gcd = (a: number, b: number): number => b ? gcd(b, a % b) : a;
// parseAr('--ar 1080:960', '1080:960')
// '--ar 9:8'
const parseAr = (ar: string, arNumber: string) => {
if (!ar) return '';
const arr = arNumber.split(':');
const a = Number(arr[0]);
const b = Number(arr[1]);
const gb = gcd(a, b);
return `--ar ${a/gb}:${b/gb}`;
}
// mj will parse prompt like put url into shortlink <shorurl>, and transform ar
const parsePrompt = (prompt: string = '') => {
return prompt.replace(/^<[^<]+>\s/, '')
// --ar 40:20 will be transformed into --ar 2:1
.replace(/--ar ([\S]+)/i, parseAr);
}
export const content2prompt = (content: string) => {
if (!content) return "";
const pattern = /\*\*(.*?)\*\*/; // Match **middle content
const matches = content.match(pattern);
if (matches && matches.length > 1 && matches[1]) {
const prompt = matches[1]; // Get the matched content
// url will be transformed into <short url>
return parsePrompt(prompt);
} else {
console.log("No match found.", content);
return parsePrompt(content);
}
};
export function custom2Type(custom: string) {
if (custom.includes("upsample")) {
return "upscale";
} else if (custom.includes("variation")) {
const list = custom.split('::');
return list[2] || "variation";
} else if (custom.includes("reroll")) {
return "reroll";
} else if (custom.includes("CustomZoom")) {
return "customZoom";
} else if (custom.includes("Outpaint")) {
return "outpaint";
} else if (custom.includes("remaster")) {
return "reroll";
} else if (custom.includes("pan_")) {
return "pan";
} else {
const list = custom.split('::');
return list[2];
}
}
export const toRemixCustom = (customID: string) => {
const parts = customID.split("::");
const convertedString = `MJ::RemixModal::${parts[4]}::${parts[3]}::0`;
return convertedString;
};
export async function base64ToBlob(base64Image: string): Promise<Blob> {
// 移除 base64 图像头部信息
const base64Data = base64Image.replace(
/^data:image\/(png|jpeg|jpg);base64,/,
""
);
// 将 base64 数据解码为二进制数据
const binaryData = atob(base64Data);
// 创建一个 Uint8Array 来存储二进制数据
const arrayBuffer = new ArrayBuffer(binaryData.length);
const uint8Array = new Uint8Array(arrayBuffer);
for (let i = 0; i < binaryData.length; i++) {
uint8Array[i] = binaryData.charCodeAt(i);
}
// 使用 Uint8Array 创建 Blob 对象
return new Blob([uint8Array], { type: "image/png" }); // 替换为相应的 MIME 类型
}