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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,14 @@ lock a direction in one click, search the full library, or keep the default.

No separate OpenAI API key or `.env` file is required for the default Codex workflow.

To use the MiniMax image backend, configure the server environment with
`CODEX_SLIDES_IMAGE_PROVIDER=minimax` and `MINIMAX_API_KEY`. Set
`MINIMAX_API_REGION` to `global_en` or `cn_zh`, and optionally set
`MINIMAX_IMAGE_MODEL` to `image-01` or `image-01-live`. The backend accepts
`MINIMAX_IMAGE_RESPONSE_FORMAT=url` or `base64`; set both
`MINIMAX_IMAGE_WIDTH` and `MINIMAX_IMAGE_HEIGHT` to request explicit dimensions
instead of the project aspect ratio.

### Install from the GitHub marketplace

This repository includes a Codex marketplace manifest, so the repository itself can be added
Expand Down
52 changes: 52 additions & 0 deletions src/lib/image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
generateSlideImage as generateCodexSlideImage,
type GenerateImageOptions as CodexImageOptions,
} from "./codex-image";
import {
generateMiniMaxImage,
miniMaxImageOptionsFromEnv,
type MiniMaxImageOptions,
} from "./minimax-image";

export type ImageProvider = "codex" | "minimax";

export interface GenerateImageOptions extends CodexImageOptions {
provider?: ImageProvider;
miniMax?: MiniMaxImageOptions;
}

export function resolveImageProvider(
configured = process.env.CODEX_SLIDES_IMAGE_PROVIDER,
): ImageProvider {
const provider = configured?.trim().toLowerCase() || "codex";
if (provider === "codex" || provider === "minimax") return provider;
throw new Error("Unsupported image provider: " + provider);
}

export function imageProviderSupportsReferences(
configured = process.env.CODEX_SLIDES_IMAGE_PROVIDER,
): boolean {
return resolveImageProvider(configured) === "codex";
}

export async function generateSlideImage(
prompt: string,
options: GenerateImageOptions = {},
): Promise<Buffer> {
const provider = options.provider ?? resolveImageProvider();
if (provider === "codex") return generateCodexSlideImage(prompt, options);

if (options.refImages?.length) {
throw new Error("The MiniMax text-to-image backend does not accept reference images");
}

const envOptions = miniMaxImageOptionsFromEnv();
const miniMaxOptions = options.miniMax ?? {};
return generateMiniMaxImage(prompt, {
...envOptions,
...miniMaxOptions,
model: miniMaxOptions.model ?? envOptions.model,
aspectRatio: miniMaxOptions.aspectRatio ?? String(options.aspect ?? "16:9"),
signal: options.signal,
});
}
124 changes: 124 additions & 0 deletions src/lib/minimax-image.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import assert from "node:assert/strict";
import test from "node:test";

import {
buildMiniMaxImageRequest,
generateMiniMaxImage,
miniMaxImageOptionsFromEnv,
parseMiniMaxImageResponse,
} from "./minimax-image.ts";

function jsonResponse(value) {
return new Response(JSON.stringify(value), {
status: 200,
headers: { "content-type": "application/json" },
});
}

test("builds a global aspect-ratio request with required fields", async () => {
const calls = [];
const imageBytes = Buffer.from("global-image");
const bytes = await generateMiniMaxImage("Draw a title slide", {
apiKey: "local-test-key",
fetchImpl: async (url, init) => {
calls.push({ url: String(url), init });
return jsonResponse({
base_resp: { status_code: 0 },
data: { image_urls: [imageBytes.toString("base64")] },
});
},
aspectRatio: "16:9",
});

assert.deepEqual(bytes, imageBytes);
assert.equal(calls.length, 1);
assert.equal(calls[0].url, "https://api.minimax.io/v1/image_generation");
assert.equal(calls[0].init.headers.Authorization, "Bearer local-test-key");
assert.deepEqual(JSON.parse(calls[0].init.body), {
model: "image-01",
prompt: "Draw a title slide",
response_format: "base64",
aspect_ratio: "16:9",
});
});

test("uses the China endpoint, live model, explicit dimensions, and URL output", async () => {
const calls = [];
const imageBytes = Buffer.from("china-image");
const bytes = await generateMiniMaxImage("Draw a product slide", {
apiKey: "local-test-key",
region: "cn_zh",
model: "image-01-live",
responseFormat: "url",
width: 1200,
height: 800,
fetchImpl: async (url, init) => {
calls.push({ url: String(url), init });
if (calls.length === 1) {
return jsonResponse({
base_resp: { status_code: 0 },
data: { image_urls: ["https://images.example.test/generated.png"] },
});
}
return new Response(imageBytes, { status: 200 });
},
});

assert.deepEqual(bytes, imageBytes);
assert.equal(calls[0].url, "https://api.minimaxi.com/v1/image_generation");
assert.deepEqual(JSON.parse(calls[0].init.body), {
model: "image-01-live",
prompt: "Draw a product slide",
response_format: "url",
width: 1200,
height: 800,
});
assert.equal(calls[1].url, "https://images.example.test/generated.png");
});

test("decodes a data URL response", async () => {
const imageBytes = Buffer.from("data-url-image");
const result = await parseMiniMaxImageResponse({
base_resp: { status_code: 0 },
data: { image_urls: ["data:image/png;base64," + imageBytes.toString("base64")] },
});
assert.deepEqual(result, imageBytes);
});

test("reads the supported backend settings from the environment", () => {
const options = miniMaxImageOptionsFromEnv({
MINIMAX_API_KEY: "local-test-key",
MINIMAX_API_REGION: "cn_zh",
MINIMAX_IMAGE_MODEL: "image-01-live",
MINIMAX_IMAGE_RESPONSE_FORMAT: "url",
MINIMAX_IMAGE_WIDTH: "1200",
MINIMAX_IMAGE_HEIGHT: "800",
});
assert.equal(options.apiKey, "local-test-key");
assert.equal(options.region, "cn_zh");
assert.equal(options.model, "image-01-live");
assert.equal(options.responseFormat, "url");
assert.equal(options.width, 1200);
assert.equal(options.height, 800);
});

test("rejects unsupported models and incomplete dimensions", () => {
assert.throws(
() => buildMiniMaxImageRequest("prompt", { apiKey: "local-test-key", model: "unknown-model" }),
/Unsupported MiniMax image model/,
);
assert.throws(
() => buildMiniMaxImageRequest("prompt", { apiKey: "local-test-key", width: 1200 }),
/width and height must be provided together/,
);
});

test("surfaces a non-zero API status", async () => {
await assert.rejects(
() => parseMiniMaxImageResponse({
base_resp: { status_code: 1001 },
data: { image_urls: [] },
}),
/status code 1001/,
);
});
Loading