Fun with MiniMax API
3 min read β’ September 30, 2024 (November 4, 2024)
Table of contents
Zoom in on a cute red shiny robot holding a white banner with text 'useapi.net' standing in the big data center, in the Pixar animation style.
Introduction
As you probably already figured out, it takes many attempts before you get your perfect 5-second video. This trial and error cycle can be very tedious and time-consuming. But it does not have to be, enter the bright and shiny world of Automation. With a simple Node.js script provided below and some basic file editing, you can cut out all the waiting time and get straight to picking the winners.
In this article, we will show how to use an experimental MiniMax API for MiniMax β’ Hailuo AI to batch-generate videos.
Preparing prompts and images
The first step will be to create a file videos.json
as shown below and edit it so it contains desired prompts. You can put as many prompt variations as you can think of. No need to hold back, as you will not be running them manually, not anymore.
It may help to ask ChatGPT, Claude or perhaps Gemini to build prompts for you. This way, you can build a lot of prompts very quickly. Later, you can see what is working and whatβs not, and refine them.
You can provide an image in addition to or instead of your text prompt. Currently .png
and .jpeg
images up to 5MB in size are supported. You can also use .webp
images, just change the extension to .jpeg
.
Give our experimental Midjourney API a try to get initial images generated from your prompt. This can help drive your video generation in a more precise way.
If an image file is provided (parameter file
), you can omit the text prompt (parameter prompt
).
Expand videos.json
[
{
"file": "./blonde.jpeg",
"prompt": "A beautiful blonde woman with striking blue eyes and an hourglass figure posing confidently for a magazine cover."
},
{
"prompt": "From below shot of a cat catholic priest performing an exorcism on a demonic cat, a parody on The Exorcist movie."
}
]
Executing script
The real magic starts here. We assume that you subscribed to useapi.net. Our subscription is essentially free when you consider the time you will save. It takes only a few minutes to set up MiniMax with our API.
π With an Unlimited plan or Standard plan for first 3 days unlimited bonus mode, you can expect the following numbers:
- 1 generation completed within 2β¦3 minutes
- 10 generations take about 20β¦30 minutes to complete
- 30 generations take about 60β¦90 minutes to complete
Free accounts take a lot longer to generate, and you may need to add multiple free accounts to get a reasonable number of generations per day.
Finally, if you are curious, you can glance over the very detailed documentation we provide. Each endpoint provides the ability to Try It right from the browser. If you donβt feel like reading the docs, that is fine too, the script below is all you need.
We will be using Node.js to execute JavaScript script provided below. Please download and install version 21 or older.
Create a file videos.mjs
with the code provided below.
Your videos.json
should be in the same folder as videos.mjs
.
Finally, execute the script node ./videos.mjs API_TOKEN
where API_TOKEN
is your useapi.net API token.
All generated videos will be downloaded locally to your drive, so you can view them once they are ready.
Expand videos.mjs
/*
Script version 2.3, November 4, 2024
Script to generate videos using prompts with MiniMax API by useapi.net π
For more details visit https://useapi.net/docs/api-minimax-v1
Installation Instructions:
==========================
You need Node.js v21 or newer installed to run this script. Download and install Node.js from:
- Windows, macOS, Linux: https://nodejs.org/
After installation, verify by running the following commands in a terminal:
node -v
Running the Script:
===================
Usage: node videos.mjs <API_TOKEN> [PROMPTS_FILE]
Replace API_TOKEN with your actual useapi.net API token, see https://useapi.net/docs/start-here/setup-useapi
If optional PROMPTS_FILE not provided videos.json will be used.
Example #1:
--------
node videos.mjs user:1234-abcdefhijklmnopqrstuv
This command executes the script using API token user:1234-abcdefhijklmnopqrstuv
Example #2:
--------
node videos.mjs user:1234-abcdefhijklmnopqrstuv myprompts.json
This command executes the script using API token user:1234-abcdefhijklmnopqrstuv and load prompts from myprompts.json file.
Changelog:
==========
- October 23, 2024: Response code 596 handling added https://useapi.net/docs/api-minimax-v1/post-minimax-videos-create#responses.
- October 25, 2024: The param prompt is optional and no longer needed if a fileID is provided.
- November 4, 2024: Retry on 502 and 504.
*/
import readline from 'node:readline';
import fs from 'fs/promises';
import { writeFile } from 'node:fs/promises';
import { Readable } from 'node:stream';
// Constants
const RESULTS_FILE = 'videos_results.txt';
const ERRORS_FILE = 'videos_errors.txt';
const SLEEP_429 = 20 * 1000; // in milliseconds
const SLEEP_DOWNLOAD = 30 * 1000; // in milliseconds
const urlAccounts = 'https://api.useapi.net/v1/minimax/accounts';
const urlAvailable = 'https://api.useapi.net/v1/minimax/scheduler/available';
const urlCreate = 'https://api.useapi.net/v1/minimax/videos/create';
const urlDownload = 'https://api.useapi.net/v1/minimax/videos/';
const urlUploadFile = 'https://api.useapi.net/v1/minimax/files/?account=';
// To upload .webp rename it to .jpeg
const supportedFileExtensions = ['png', 'jpeg']
// account: { filename: fileID }
const uploadedFiles = {};
let availableAccountsCount = 0;
// Track accounts without any credits left
const outOfCredits = [];
// Utility to sleep for given milliseconds
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Function to fetch configured MiniMax API accounts
async function fetchAccounts(apiToken) {
const response = await fetch(urlAccounts, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${apiToken}`
}
});
if (!response.ok) {
console.error(`β Error fetching accounts (HTTP ${response.status}): ${response.statusText}`);
process.exit(1);
}
return response.json();
}
const elapsedTimeSec = (start) => (Date.now() - start) / 1000;
async function uploadFile(apiToken, account, filename) {
if (!uploadedFiles[account])
uploadedFiles[account] = {};
// Check if already uploaded for provided account
if (uploadedFiles[account].hasOwnProperty(filename))
return uploadedFiles[account][filename];
const startTime = Date.now();
console.log(`β¬οΈ Account ${account} uploading fileβ¦`, filename);
const body = new Blob([await fs.readFile(filename)]);
const fileExt = filename.split('.').pop();
const response = await fetch(`${urlUploadFile}${account}`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${apiToken}`,
'Content-Type': `image/${fileExt}`
},
body
});
if (response.ok) {
const json = await response.json();
console.log(`π fileID (${elapsedTimeSec(startTime)} sec)`, json.fileID);
uploadedFiles[account][filename] = json.fileID;
}
else {
console.error(`β Unable to upload file HTTP ${response.status} (${elapsedTimeSec(startTime)} sec)`, await response.text());
// Do not attempt to upload failed file again
uploadedFiles[account][filename] = undefined;
}
return uploadedFiles[account][filename];
}
// Function to submit a prompt
async function submitPrompt(apiToken, prompt, filename, promptIndex) {
console.log(`\nπ Prompt #${promptIndex}: ${prompt}`);
const availableResponse = await fetch(urlAvailable, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${apiToken}`
}
});
const availableJSON = await availableResponse.json();
console.log(`Currently executing ${availableJSON.executing.length} generation(s).`);
const available = availableJSON.available
.filter(a => !outOfCredits.includes(a.account));
console.log(`Available accounts ${available.length}:`, available.map(a => `${a.account} (${a.available})`).join(', '));
if (available.length == 0) {
console.log(`ποΈ Waiting for currently running generations to complete β¦`);
return 429;
}
const account = available[0].account;
const fileID = filename ? await uploadFile(apiToken, account, filename) : undefined;
const info = `Prompt #${promptIndex} account ${account}`;
console.log(`${info} β¦`);
const createResponse = await fetch(urlCreate, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiToken}`
},
body: JSON.stringify({ account, prompt, fileID })
});
const createBody = await createResponse.text();
if (createResponse.status == 200) {
const json = JSON.parse(createBody);
const videoId = json.videoId;
if (videoId) {
await fs.appendFile(RESULTS_FILE, `${videoId},${prompt}\n`);
return 200;
} else {
const error = `No videoId found in HTTP 200 response`;
console.log(`β ${info}: ${error}`, createBody);
await fs.appendFile(ERRORS_FILE, `${error},${prompt}\n`);
return 500;
}
} else {
let returnStatus = createResponse.status;
switch (createResponse.status) {
case 502: // Happens when MiniMax website is too busy
case 504: // Happens when MiniMax website is too busy
case 429:
console.log(`ποΈ ${info}: retry on HTTP ${createResponse.status}`);
returnStatus = 429;
break;
case 422:
console.log(`π ${info}: MODERATED prompt`, createBody);
await fs.appendFile(ERRORS_FILE, `${createResponse.status},${prompt}\n`);
break;
case 412:
console.log(`π ${info}: account run out of credits`, createBody);
outOfCredits.push(account);
break;
default:
console.log(`β ${info}: FAILED with HTTP ${createResponse.status}`, createBody);
await fs.appendFile(ERRORS_FILE, `${createResponse.status},${prompt}\n`);
}
return returnStatus;
}
}
// Function to download videos based on VIDEO IDs
async function download(apiToken) {
try {
const resultsContent = await fs.readFile(RESULTS_FILE, 'utf8');
const lines = resultsContent.trim().split('\n');
for (const line of lines) {
const [videoId, prompt] = line.split(',');
const videoFilename = `${videoId.replace(/:/g, '_')}.mp4`;
console.log(`π ${videoId}`);
try {
await fs.access(videoFilename);
console.log(`β οΈ ${videoFilename} already exists. Skipping download.`);
continue;
} catch {
// File does not exist, proceed with downloading
}
while (true) {
const response = await fetch(`${urlDownload}${videoId}`, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${apiToken}`
}
});
if (!response.ok) {
console.log(`π MODERATED ${videoId} (HTTP ${response.status}):\n${prompt}\n`, await response.text());
break;
}
const taskResponseBody = await response.json();
const { status, statusFinal, statusLabel, downloadURL, videoURL, percent } = taskResponseBody;
if (statusFinal) {
const url = downloadURL ?? videoURL;
if (url) {
console.log(`β
Downloading ${url} to ${videoFilename}`);
try {
const videoResponse = await fetch(url);
if (!videoResponse.ok) {
console.error(`β Unable to download ${videoId} (HTTP ${videoResponse.status}):\n${prompt}\n`, url);
break;
}
const stream = Readable.fromWeb(videoResponse.body);
await writeFile(videoFilename, stream);
} catch (err) {
console.error(`β Error during download: ${err}`);
}
} else
console.error(`π Unable to download ${videoId} status (${status} ${statusLabel}):\n${prompt}\n`);
break;
} else {
console.log(`β ${videoId} status (${status} ${statusLabel}) and is still in progress (${percent}%), waitingβ¦`);
await sleep(SLEEP_DOWNLOAD);
}
}
}
} catch (error) {
console.log(`β Error during download:`, error.stack || error);
}
}
// Main function
async function main() {
const apiToken = process.argv[2];
const promptFile = process.argv[3] || 'videos.json'; // Default to 'videos.json' if not provided
if (!apiToken) {
console.error('Usage: node videos.mjs <API_TOKEN> [PROMPTS_FILE]');
process.exit(1);
}
console.log('Script v2.3');
console.log('Node version is: ' + process.version);
try {
if (await fileExists(RESULTS_FILE)) {
let user_input;
while (!['y', 'n'].includes(user_input)) {
user_input = (await promptUser(`β ${RESULTS_FILE} file detected. Do you want to download the results now? (y/n): `))?.toLowerCase();
if (user_input == 'y') {
await download(apiToken);
await fs.unlink(RESULTS_FILE);
}
}
}
const start = new Date();
try {
console.info('START EXECUTION', start);
await execute(apiToken, promptFile); // Pass the promptFile to execute function
}
finally {
console.info('COMPLETED', new Date());
console.info('EXECUTION ELAPSED', diffInMinutesAndSeconds(start, new Date()));
}
try {
console.info('START DOWNLOAD', start);
await download(apiToken);
}
finally {
console.info('TOTAL ELAPSED', diffInMinutesAndSeconds(start, new Date()));
}
} catch (error) {
console.error('β Error during execution:', error.stack || error);
}
}
async function execute(apiToken, promptFile) {
const accounts = await fetchAccounts(apiToken);
const videoAccounts = Object.values(accounts)
.filter(a => a.supportVideo) // Accounts with video support
.filter(a => !a.error); // Active accounts without error
console.info(`Configured active MiniMax API video accounts`, videoAccounts.length);
if (videoAccounts.length <= 0) {
console.error(`β No configured active video accounts found. Please refer to https://useapi.net/docs/start-here/setup-useapi`);
process.exit(1);
}
availableAccountsCount = videoAccounts.length;
const promptData = await fs.readFile(promptFile, 'utf8');
const prompts = JSON.parse(promptData);
console.log(`Total number of prompts to process`, prompts.length);
let warnings = [];
// First pass: check for warnings
for (let i = 0; i < prompts.length; i++) {
const { file, prompt } = prompts[i];
if (!prompt && !file) {
warnings.push(`β οΈ Skip empty prompt with empty file at index ${i}`);
continue;
}
if (file) {
try {
await fs.access(file);
} catch {
warnings.push(`β οΈ Specified file '${file}' does not exist. Skip prompt ${i}`);
continue;
}
const ext = file.split('.').pop();
if (!supportedFileExtensions.includes(ext)) {
warnings.push(`β οΈ File ${file} extension ${ext} now supported. Skip prompt ${i}`);
continue;
}
}
}
if (warnings.length > 0) {
warnings.forEach(warning => console.warn(warning));
console.error(`β Execution stopped due to warnings.`);
process.exit(1);
}
for (let i = 0; i < prompts.length; i++) {
const { prompt, file } = prompts[i];
while (true) {
const responseCode = await submitPrompt(apiToken, prompt, file, i + 1);
if (responseCode == 429)
await sleep(SLEEP_429);
else
if (responseCode == 412) {
// Check if there's no accounts left at all
if (availableAccountsCount == outOfCredits.length) {
console.error(`β All configured video accounts run out of credits`);
process.exit(1);
}
} else
if (responseCode == 596) {
console.error(`β Your hailuoai.video account has been placed on hold, which may last a few hours. It may be a good idea to pause operations until then.`);
process.exit(1);
} else
break;
}
}
}
// Utility function to check if a file exists
async function fileExists(path) {
try {
await fs.access(path);
return true;
} catch {
return false;
}
}
// Function to prompt user input
async function promptUser(query) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => rl.question(query, answer => {
rl.close();
resolve(answer);
}));
}
function diffInMinutesAndSeconds(date1, date2) {
const diffInSeconds = Math.floor((date2 - date1) / 1000);
return `${Math.floor(diffInSeconds / 60)} minutes ${diffInSeconds % 60} seconds`;
};
main();
Examples
An beautiful supermodel with an embroidered bio-exoskeleton, posed dynamically against a black background.
A futuristic, cyberpunk-inspired video featuring a woman with a bold yellow and black ensemble.
Ghost Rider riding chrome-detailed motorcycle.
From below shot of a cat catholic priest performing an exorcism on a demonic cat, a parody on The Exorcist movie.
Surreal fantasy world: light pink sky, ocean made entirely of glowing sparkling water, focus on Daenerys Targaryen quickly raising from the ocean, as she looks straight at the camera. Her skin is smooth and moist, she has shoulder-long platinum blonde wet hair, her tummy is toned. Eyes deep green color almost glowing. Sparkling liquid surrounding her because surreal ocean is composed of glowing sparkling water.
Camera zooming slowly on a very pretty lady wearing pink bikini, she is jogging on the beach.
Hungry snake hunting the mouse.
From below tracking shot of a tall man from his back walking down a long symmetrical alley with trees on both sides. He is wearing a long bright blue colored trench coat. The trees are magnificent boasting wide branches filled with abundant yellow leaves. It is windy so the manβs coat and leaves on the trees are billowing, moving.
Conclusion
Visit our Discord Server or Telegram Channel for any support questions and concerns.
We regularly post guides and tutorials on the YouTube Channel.
Check our GitHub repo with code examples.