Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ AI_GATEWAY_API_KEY=
ELEVENLABS_API_KEY=
FFMPEG_BIN=

# Optional image-to-image generation for artwork scripts.
MINIMAX_API_KEY=
MINIMAX_REGION=global_en
MINIMAX_SUBJECT_REFERENCE_URL=

# Redis / rate-limit / cache
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
Expand Down
19 changes: 19 additions & 0 deletions scripts/generate-announcement-banner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import path from "node:path";
import OpenAI from "openai";
import sharp from "sharp";

import { generateMiniMaxReferenceImageFromEnv } from "./minimax-image-generation";

const OUTPUT_PATH = path.join(
process.cwd(),
"public",
Expand All @@ -35,6 +37,23 @@ Mood: collect-them-all energy. Like a Pokémon Red box art reimagined for cute p
Aspect ratio 16:9. Sharp pixel detail. Empty top-left corner area for overlay text.`;

async function main() {
const miniMaxBuffer = await generateMiniMaxReferenceImageFromEnv(
PROMPT,
"16:9",
);
if (miniMaxBuffer) {
console.log(
`[banner] received ${miniMaxBuffer.length} bytes from MiniMax, transcoding to webp...`,
);
const webp = await sharp(miniMaxBuffer)
.resize(1280, 720, { fit: "cover", kernel: sharp.kernel.lanczos3 })
.webp({ quality: 88 })
.toBuffer();
await writeFile(OUTPUT_PATH, webp);
console.log(`[banner] wrote ${OUTPUT_PATH} (${webp.length} bytes)`);
return;
}

const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error("OPENAI_API_KEY is not set");
Expand Down
13 changes: 13 additions & 0 deletions scripts/generate-discord-icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import path from "node:path";
import OpenAI from "openai";
import sharp from "sharp";

import { generateMiniMaxReferenceImageFromEnv } from "./minimax-image-generation";

const OUTPUT_PATH = path.join(
process.cwd(),
"public",
Expand All @@ -29,6 +31,17 @@ Style: 16-bit chibi pixel art, clean outlines, friendly mascot energy, modern fl
No text, no letters, no watermarks, no UI chrome.`;

async function main() {
const miniMaxBuffer = await generateMiniMaxReferenceImageFromEnv(PROMPT, "1:1");
if (miniMaxBuffer) {
console.log(
`[icon] received ${miniMaxBuffer.length} bytes from MiniMax, normalizing to PNG...`,
);
const png = await sharp(miniMaxBuffer).resize(1024, 1024).png().toBuffer();
await writeFile(OUTPUT_PATH, png);
console.log(`[icon] wrote ${OUTPUT_PATH} (${png.length} bytes)`);
return;
}

const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) throw new Error("OPENAI_API_KEY is not set");

Expand Down
101 changes: 101 additions & 0 deletions scripts/minimax-image-generation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { describe, expect, test } from "bun:test";

import {
generateMiniMaxImage,
MiniMaxImageGenerationError,
validateMiniMaxImageToImageRequest,
} from "./minimax-image-generation";

const request = {
model: "image-01" as const,
prompt: "Keep the character recognizable in a new scene",
subject_reference: [
{
type: "character" as const,
image_file: "https://example.com/reference.png",
},
],
aspect_ratio: "1:1" as const,
response_format: "url" as const,
};

describe("MiniMax image generation", () => {
test("validates image-to-image inputs", () => {
expect(() => validateMiniMaxImageToImageRequest(request)).not.toThrow();
expect(() =>
validateMiniMaxImageToImageRequest({
...request,
subject_reference: [],
}),
).toThrow(MiniMaxImageGenerationError);
expect(() =>
validateMiniMaxImageToImageRequest({
...request,
width: 1024,
}),
).toThrow("width and height");
});

test("posts to the selected regional endpoint", async () => {
let requestedUrl = "";
const fetchImpl: typeof fetch = async (input, init) => {
requestedUrl = input.toString();
expect(init?.method).toBe("POST");
expect((init?.headers as Record<string, string>).Authorization).toBe(
"Bearer test-key",
);
expect(JSON.parse(init?.body as string).subject_reference).toEqual(
request.subject_reference,
);
return Response.json({
data: { image_urls: ["https://example.com/generated.png"] },
metadata: { success_count: "1", failed_count: "0" },
base_resp: { status_code: 0, status_msg: "success" },
});
};

const response = await generateMiniMaxImage(request, {
apiKey: "test-key",
region: "cn_zh",
fetch: fetchImpl,
});

expect(requestedUrl).toBe("https://api.minimaxi.com/v1/image_generation");
expect(response.data.image_urls).toHaveLength(1);
});

test("accepts base64 image responses", async () => {
const response = await generateMiniMaxImage(
{ ...request, response_format: "base64" },
{
apiKey: "test-key",
fetch: async (input) => {
expect(input.toString()).toBe(
"https://api.minimax.io/v1/image_generation",
);
return Response.json({
data: { image_base64: ["aW1hZ2U="] },
metadata: { success_count: 1, failed_count: 0 },
base_resp: { status_code: 0 },
});
},
},
);

expect(response.data.image_base64).toEqual(["aW1hZ2U="]);
});

test("rejects an unsuccessful API response", async () => {
await expect(
generateMiniMaxImage(request, {
apiKey: "test-key",
fetch: async () =>
Response.json({
data: {},
metadata: { success_count: 0, failed_count: 1 },
base_resp: { status_code: 1001, status_msg: "Invalid request" },
}),
}),
).rejects.toThrow("Invalid request");
});
});
Loading