From 04d1ae026942b8bdbaebce06c228221ed07abb22 Mon Sep 17 00:00:00 2001 From: Morpheus Date: Sat, 22 Aug 2026 22:21:33 +0000 Subject: [PATCH] Add MiniMax 8s chunk/audio matrix queue script --- ...ueue-minimax-r2v-8s-chunk-audio-matrix.mjs | 304 ++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 scripts/queue-minimax-r2v-8s-chunk-audio-matrix.mjs diff --git a/scripts/queue-minimax-r2v-8s-chunk-audio-matrix.mjs b/scripts/queue-minimax-r2v-8s-chunk-audio-matrix.mjs new file mode 100644 index 0000000..a401e75 --- /dev/null +++ b/scripts/queue-minimax-r2v-8s-chunk-audio-matrix.mjs @@ -0,0 +1,304 @@ +#!/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-8s-chunk-audio-matrix'; +const DURATION_SECONDS = 8; +const AUDIO_DENOISE_VALUES = [0.2, 0.35, 0.5]; +const CHUNK_VALUES = [2, 4, 8]; + +function printUsage() { + console.log(`Usage: + node scripts/queue-minimax-r2v-8s-chunk-audio-matrix.mjs [options] + +Options: + --server ComfyUI base URL. Default: ${DEFAULT_SERVER} + --scene-image Local scene reference image. Default: ${DEFAULT_SCENE_REF} + --character-image Local character reference image. Default: ${DEFAULT_CHARACTER_REF} + --steps First-pass steps. Default: value from api-prompt.json + --prefix Output filename prefix root. Default: MiniMax/r2v-8s-chunk-audio-matrix + --out-dir Run artifact directory. Default: ${DEFAULT_OUT_ROOT}/ + --dry-run Resolve prompts locally but do not submit +`); +} + +function parseArgs(argv) { + const options = { + server: DEFAULT_SERVER, + sceneImage: DEFAULT_SCENE_REF, + characterImage: DEFAULT_CHARACTER_REF, + steps: null, + prefix: 'MiniMax/r2v-8s-chunk-audio-matrix', + 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 '--steps': options.steps = Math.trunc(Number(take())); break; + case '--prefix': options.prefix = take(); break; + case '--out-dir': options.outDir = take(); break; + default: + throw new Error(`Unknown argument: ${arg}`); + } + } + + 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 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(); +} + +function buildVariants(basePrompt, options) { + const baseSteps = options.steps ?? Math.trunc(Number(basePrompt['566']?.inputs?.value ?? 8)); + const variants = [ + { + slug: 'ctrl-current', + refImageSize: 'max', + twoPass: false, + highQuality: false, + headChunks: 4, + ffChunks: 4, + audioDenoise: Number(basePrompt['571:531']?.inputs?.audio_denoise ?? 0.35), + steps: baseSteps, + notes: 'Current maintained behavior except duration forced to 8s.', + }, + { + slug: 'ctrl-2pass-match', + refImageSize: 'match', + twoPass: true, + highQuality: false, + headChunks: 4, + ffChunks: 4, + audioDenoise: 0.35, + steps: baseSteps, + notes: 'Baseline for the matrix with match refs and 2 Pass enabled.', + }, + ]; + + for (const headChunks of CHUNK_VALUES) { + for (const ffChunks of CHUNK_VALUES) { + for (const audioDenoise of AUDIO_DENOISE_VALUES) { + variants.push({ + slug: `h${headChunks}-f${ffChunks}-a${String(audioDenoise).replace('.', '')}`, + refImageSize: 'match', + twoPass: true, + highQuality: false, + headChunks, + ffChunks, + audioDenoise, + steps: baseSteps, + notes: 'Chunk/audio matrix job.', + }); + } + } + } + + return variants; +} + +function buildPrompt(basePrompt, variant, uploadedRefs, options) { + const prompt = clone(basePrompt); + const width = Math.trunc(Number(prompt['576']?.inputs?.width)); + const height = Math.trunc(Number(prompt['576']?.inputs?.height)); + + prompt['574'].inputs.image = uploadedRefs.scene; + prompt['575'].inputs.image = uploadedRefs.character; + prompt['576'].inputs.width = width; + prompt['576'].inputs.height = height; + prompt['576'].inputs.length = secondsToFrames(DURATION_SECONDS); + prompt['576'].inputs.ref_image_size = variant.refImageSize; + prompt['514'].inputs.ResolutionState = JSON.stringify({ + mode: 'custom', + ratio: 'custom', + w: width, + h: height, + custom_w: width, + custom_h: height, + custom_ratio_w: width, + custom_ratio_h: height, + snap: 16, + }); + prompt['550:537'].inputs.value = DURATION_SECONDS; + prompt['566'].inputs.value = variant.steps; + prompt['548'].inputs.value = variant.twoPass; + prompt['715'].inputs.value = variant.highQuality; + prompt['680:497'].inputs.head_chunks = variant.headChunks; + prompt['680:498'].inputs.chunks = variant.ffChunks; + prompt['571:531'].inputs.audio_denoise = variant.audioDenoise; + prompt['539'].inputs.noise_seed = 9100 + options.variantIndex; + prompt['591'].inputs.string_b = String(prompt['591'].inputs.string_b).replace( + /A clean \d+-second cinematic shot/, + `A clean ${DURATION_SECONDS}-second cinematic shot`, + ); + prompt['602'].inputs.filename_prefix = `${options.prefix}/${variant.slug}`; + + return prompt; +} + +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 = buildVariants(basePrompt, options); + + 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), + durationSeconds: DURATION_SECONDS, + prefix: options.prefix, + dryRun: options.dryRun, + stepsOverride: options.steps, + }, + variants, + }); + + const summary = []; + for (const [index, variant] of variants.entries()) { + const prompt = buildPrompt(basePrompt, variant, uploadedRefs, { prefix: options.prefix, variantIndex: index }); + 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: DURATION_SECONDS, + frameLength: prompt['576'].inputs.length, + refImageSize: variant.refImageSize, + twoPass: variant.twoPass, + highQuality: variant.highQuality, + headChunks: variant.headChunks, + ffChunks: variant.ffChunks, + audioDenoise: variant.audioDenoise, + steps: variant.steps, + 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: DURATION_SECONDS, + frameLength: prompt['576'].inputs.length, + refImageSize: variant.refImageSize, + twoPass: variant.twoPass, + highQuality: variant.highQuality, + headChunks: variant.headChunks, + ffChunks: variant.ffChunks, + audioDenoise: variant.audioDenoise, + steps: variant.steps, + 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); +});