Files
comfyui-workflows/scripts/queue-minimax-r2v-duration-sweep.mjs
T

282 lines
9.9 KiB
JavaScript

#!/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
--upscale-multiplier <n> Override RTX/HQ upscale multiplier. Default: prompt value
--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,
upscaleMultiplier: null,
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 '--upscale-multiplier': options.upscaleMultiplier = Number(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;
if (variant.upscaleMultiplier !== null) {
prompt['723'].inputs.value = variant.upscaleMultiplier;
}
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,
upscaleMultiplier: options.upscaleMultiplier,
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,
upscaleMultiplier: options.upscaleMultiplier,
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,
upscaleMultiplier: variant.upscaleMultiplier,
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,
upscaleMultiplier: variant.upscaleMultiplier,
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);
});