Fix MiniMax R2V queue assets
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
const WORKFLOW_ROOT = path.join(REPO_ROOT, 'optimized/image-to-video/minimax-h3-r2v-master');
|
||||
const BASE_PROMPT = path.join(WORKFLOW_ROOT, 'api-prompt.json');
|
||||
const DEFAULT_SCENE_REF = path.join(WORKFLOW_ROOT, 'references/default-location-reference.jpg');
|
||||
const DEFAULT_CHARACTER_REF = path.join(WORKFLOW_ROOT, 'references/default-character-reference.jpg');
|
||||
const DEFAULT_SERVER = process.env.COMFYUI_URL || 'http://192.168.1.202:8188';
|
||||
const DEFAULT_OUT_ROOT = '/home/node/.openclaw/workspace/tmp/minimax-r2v-duration-sweep';
|
||||
const DEFAULT_WIDTH = 1088;
|
||||
const DEFAULT_HEIGHT = 608;
|
||||
const DEFAULT_STEPS = 8;
|
||||
const DEFAULT_DURATION_SECONDS = [2, 5, 8, 12, 15, 17, 20];
|
||||
|
||||
function printUsage() {
|
||||
console.log(`Usage:
|
||||
node scripts/queue-minimax-r2v-duration-sweep.mjs [options]
|
||||
|
||||
Options:
|
||||
--server <url> ComfyUI base URL. Default: ${DEFAULT_SERVER}
|
||||
--scene-image <path> Local scene reference image. Default: ${DEFAULT_SCENE_REF}
|
||||
--character-image <path> Local character reference image. Default: ${DEFAULT_CHARACTER_REF}
|
||||
--durations <csv> Comma-separated duration seconds. Default: ${DEFAULT_DURATION_SECONDS.join(',')}
|
||||
--width <int> Output width. Default: ${DEFAULT_WIDTH}
|
||||
--height <int> Output height. Default: ${DEFAULT_HEIGHT}
|
||||
--steps <int> First-pass steps. Default: ${DEFAULT_STEPS}
|
||||
--high-quality <bool> Enable HIGH QUALITY branch. Default: false
|
||||
--prefix <text> Output filename prefix root. Default: MiniMax/r2v-duration-sweep
|
||||
--out-dir <path> Run artifact directory. Default: ${DEFAULT_OUT_ROOT}/<timestamp>
|
||||
--dry-run Resolve prompts locally but do not submit
|
||||
`);
|
||||
}
|
||||
|
||||
function parseBoolean(raw) {
|
||||
const value = String(raw).trim().toLowerCase();
|
||||
if (['1', 'true', 'yes', 'on'].includes(value)) return true;
|
||||
if (['0', 'false', 'no', 'off'].includes(value)) return false;
|
||||
throw new Error(`Invalid boolean value: ${raw}`);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
server: DEFAULT_SERVER,
|
||||
sceneImage: DEFAULT_SCENE_REF,
|
||||
characterImage: DEFAULT_CHARACTER_REF,
|
||||
durations: [...DEFAULT_DURATION_SECONDS],
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_HEIGHT,
|
||||
steps: DEFAULT_STEPS,
|
||||
highQuality: false,
|
||||
prefix: 'MiniMax/r2v-duration-sweep',
|
||||
outDir: '',
|
||||
dryRun: false,
|
||||
};
|
||||
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (arg === '--help' || arg === '-h') {
|
||||
printUsage();
|
||||
process.exit(0);
|
||||
}
|
||||
if (arg === '--dry-run') {
|
||||
options.dryRun = true;
|
||||
continue;
|
||||
}
|
||||
const next = argv[i + 1];
|
||||
const take = () => {
|
||||
if (next === undefined) throw new Error(`Missing value for ${arg}`);
|
||||
i += 1;
|
||||
return next;
|
||||
};
|
||||
switch (arg) {
|
||||
case '--server': options.server = take(); break;
|
||||
case '--scene-image': options.sceneImage = take(); break;
|
||||
case '--character-image': options.characterImage = take(); break;
|
||||
case '--durations':
|
||||
options.durations = take().split(',').map((value) => Number(value.trim())).filter((value) => Number.isFinite(value) && value > 0);
|
||||
break;
|
||||
case '--width': options.width = Math.trunc(Number(take())); break;
|
||||
case '--height': options.height = Math.trunc(Number(take())); break;
|
||||
case '--steps': options.steps = Math.trunc(Number(take())); break;
|
||||
case '--high-quality': options.highQuality = parseBoolean(take()); break;
|
||||
case '--prefix': options.prefix = take(); break;
|
||||
case '--out-dir': options.outDir = take(); break;
|
||||
default:
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.durations.length) throw new Error('At least one duration is required.');
|
||||
return options;
|
||||
}
|
||||
|
||||
function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function writeJson(filePath, value) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function secondsToFrames(seconds) {
|
||||
return Math.round(seconds * 24) + 3;
|
||||
}
|
||||
|
||||
function buildPrompt(basePrompt, variant, uploadedRefs) {
|
||||
const prompt = clone(basePrompt);
|
||||
const frameLength = secondsToFrames(variant.durationSeconds);
|
||||
|
||||
prompt['574'].inputs.image = uploadedRefs.scene;
|
||||
prompt['575'].inputs.image = uploadedRefs.character;
|
||||
prompt['576'].inputs.width = variant.width;
|
||||
prompt['576'].inputs.height = variant.height;
|
||||
prompt['576'].inputs.length = frameLength;
|
||||
prompt['514'].inputs.ResolutionState = JSON.stringify({
|
||||
mode: 'custom',
|
||||
ratio: 'custom',
|
||||
w: variant.width,
|
||||
h: variant.height,
|
||||
custom_w: variant.width,
|
||||
custom_h: variant.height,
|
||||
custom_ratio_w: variant.width,
|
||||
custom_ratio_h: variant.height,
|
||||
snap: 16,
|
||||
});
|
||||
prompt['550:537'].inputs.value = variant.durationSeconds;
|
||||
prompt['566'].inputs.value = variant.steps;
|
||||
prompt['715'].inputs.value = variant.highQuality;
|
||||
prompt['539'].inputs.noise_seed = variant.seed;
|
||||
prompt['591'].inputs.string_b = prompt['591'].inputs.string_b.replace(
|
||||
/A clean 10-second cinematic shot/,
|
||||
`A clean ${variant.durationSeconds}-second cinematic shot`,
|
||||
);
|
||||
prompt['602'].inputs.filename_prefix = `${variant.prefix}/${variant.slug}`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
function guessContentType(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
|
||||
if (ext === '.webp') return 'image/webp';
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
async function uploadImage(server, filePath) {
|
||||
const absPath = path.resolve(filePath);
|
||||
const form = new FormData();
|
||||
form.set('type', 'input');
|
||||
form.set('overwrite', 'true');
|
||||
form.set('image', new Blob([fs.readFileSync(absPath)], { type: guessContentType(absPath) }), path.basename(absPath));
|
||||
|
||||
const response = await fetch(`${server.replace(/\/$/, '')}/upload/image`, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Failed to upload ${absPath} (${response.status}): ${text}`);
|
||||
}
|
||||
const payload = await response.json();
|
||||
return String(payload?.name || path.basename(absPath));
|
||||
}
|
||||
|
||||
async function submitPrompt(server, prompt) {
|
||||
const response = await fetch(`${server.replace(/\/$/, '')}/prompt`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
client_id: crypto.randomUUID(),
|
||||
prompt,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Prompt submission failed (${response.status}): ${text}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const outDir = path.resolve(options.outDir || path.join(DEFAULT_OUT_ROOT, timestamp));
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const basePrompt = JSON.parse(fs.readFileSync(BASE_PROMPT, 'utf8'));
|
||||
const uploadedRefs = {
|
||||
scene: await uploadImage(options.server, options.sceneImage),
|
||||
character: await uploadImage(options.server, options.characterImage),
|
||||
};
|
||||
|
||||
const variants = options.durations.map((durationSeconds, index) => ({
|
||||
slug: `${String(index + 1).padStart(2, '0')}-${durationSeconds}s`,
|
||||
durationSeconds,
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
steps: options.steps,
|
||||
highQuality: options.highQuality,
|
||||
seed: 8200 + index,
|
||||
prefix: options.prefix,
|
||||
}));
|
||||
|
||||
writeJson(path.join(outDir, 'run.json'), {
|
||||
createdAt: new Date().toISOString(),
|
||||
server: options.server,
|
||||
basePrompt: BASE_PROMPT,
|
||||
uploadedRefs,
|
||||
options: {
|
||||
sceneImage: path.resolve(options.sceneImage),
|
||||
characterImage: path.resolve(options.characterImage),
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
steps: options.steps,
|
||||
highQuality: options.highQuality,
|
||||
durations: options.durations,
|
||||
prefix: options.prefix,
|
||||
},
|
||||
variants,
|
||||
});
|
||||
|
||||
const summary = [];
|
||||
for (const variant of variants) {
|
||||
const prompt = buildPrompt(basePrompt, variant, uploadedRefs);
|
||||
const variantDir = path.join(outDir, variant.slug);
|
||||
fs.mkdirSync(variantDir, { recursive: true });
|
||||
writeJson(path.join(variantDir, 'prompt.json'), prompt);
|
||||
|
||||
if (options.dryRun) {
|
||||
summary.push({
|
||||
slug: variant.slug,
|
||||
durationSeconds: variant.durationSeconds,
|
||||
frameLength: prompt['576'].inputs.length,
|
||||
dryRun: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const submission = await submitPrompt(options.server, prompt);
|
||||
const promptId = String(submission?.prompt_id || '').trim();
|
||||
if (!promptId) throw new Error(`Missing prompt_id for ${variant.slug}: ${JSON.stringify(submission)}`);
|
||||
writeJson(path.join(variantDir, 'submission.json'), {
|
||||
at: new Date().toISOString(),
|
||||
promptId,
|
||||
submission,
|
||||
variant,
|
||||
});
|
||||
summary.push({
|
||||
slug: variant.slug,
|
||||
durationSeconds: variant.durationSeconds,
|
||||
frameLength: prompt['576'].inputs.length,
|
||||
promptId,
|
||||
});
|
||||
}
|
||||
|
||||
writeJson(path.join(outDir, 'summary.json'), summary);
|
||||
console.log(JSON.stringify({ outDir, uploadedRefs, summary }, null, 2));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error?.stack || error?.message || String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user