save changes

This commit is contained in:
2025-09-15 07:39:50 +02:00
parent 0521b28626
commit b153826a0d
21 changed files with 4098 additions and 5 deletions

View File

@ -8,7 +8,8 @@
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1",
"db:schema": "ts-node src/schema.ts",
"db:test": "ts-node src/testmysql.ts"
"db:test": "ts-node src/testmysql.ts",
"infinity:start": "ts-node src/infinityvideo_generator/start.ts"
},
"keywords": [],
"author": "",
@ -27,4 +28,4 @@
"puppeteer": "^24.16.2",
"uuid": "^11.1.0"
}
}
}

View File

@ -0,0 +1,195 @@
{
"8": {
"inputs": {
"samples": [
"31",
0
],
"vae": [
"39",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"9": {
"inputs": {
"filename_prefix": "FACEIMAGE",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"27": {
"inputs": {
"width": 720,
"height": 1280,
"batch_size": 1
},
"class_type": "EmptySD3LatentImage",
"_meta": {
"title": "EmptySD3LatentImage"
}
},
"31": {
"inputs": {
"seed": 161646847059712,
"steps": 20,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"denoise": 1,
"model": [
"45",
0
],
"positive": [
"41",
0
],
"negative": [
"42",
0
],
"latent_image": [
"27",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"38": {
"inputs": {
"unet_name": "flux1-krea-dev_fp8_scaled.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"39": {
"inputs": {
"vae_name": "ae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"40": {
"inputs": {
"clip_name1": "clip_l.safetensors",
"clip_name2": "t5xxl_fp16.safetensors",
"type": "flux",
"device": "default"
},
"class_type": "DualCLIPLoader",
"_meta": {
"title": "DualCLIPLoader"
}
},
"41": {
"inputs": {
"clip_l": "realistic photo of 25 years old girl , face zoom up, Neutral face, long straight hair, pastel blue and purple hair color",
"t5xxl": "realistic photo of 25 years old girl , face zoom up, Neutral face, long straight hair, pastel blue and purple hair color",
"guidance": 3.5,
"clip": [
"40",
0
]
},
"class_type": "CLIPTextEncodeFlux",
"_meta": {
"title": "CLIPTextEncodeFlux"
}
},
"42": {
"inputs": {
"conditioning": [
"41",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"45": {
"inputs": {
"weight": 0.7700000000000001,
"start_at": 0,
"end_at": 1,
"model": [
"38",
0
],
"pulid_flux": [
"46",
0
],
"eva_clip": [
"47",
0
],
"face_analysis": [
"48",
0
],
"image": [
"49",
0
]
},
"class_type": "ApplyPulidFlux",
"_meta": {
"title": "Apply PuLID Flux"
}
},
"46": {
"inputs": {
"pulid_file": "pulid_flux_v0.9.1.safetensors"
},
"class_type": "PulidFluxModelLoader",
"_meta": {
"title": "Load PuLID Flux Model"
}
},
"47": {
"inputs": {},
"class_type": "PulidFluxEvaClipLoader",
"_meta": {
"title": "Load Eva Clip (PuLID Flux)"
}
},
"48": {
"inputs": {
"provider": "CUDA"
},
"class_type": "PulidFluxInsightFaceLoader",
"_meta": {
"title": "Load InsightFace (PuLID Flux)"
}
},
"49": {
"inputs": {
"image": "Generated Image September 12, 2025 - 1_04PM.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
}
}

View File

@ -0,0 +1,349 @@
{
"6": {
"inputs": {
"text": "Create an 8-second animated loop featuring a young man sitting on a stone ledge overlooking a nighttime cityscape. The scene should begin with a slow zoom into the boys face as he gazes upwards at the starry sky. Throughout the video, have shooting stars streak across the sky some fast, some slower, creating a dynamic visual effect. Gentle wind blows his hair and clothing.",
"clip": [
"38",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Positive Prompt)"
}
},
"7": {
"inputs": {
"text": "色调艳丽过曝静态细节模糊不清字幕风格作品画作画面静止整体发灰最差质量低质量JPEG压缩残留丑陋的残缺的多余的手指画得不好的手部画得不好的脸部畸形的毁容的形态畸形的肢体手指融合静止不动的画面杂乱的背景三条腿背景人很多倒着走",
"clip": [
"38",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Negative Prompt)"
}
},
"8": {
"inputs": {
"samples": [
"58",
0
],
"vae": [
"39",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"38": {
"inputs": {
"clip_name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors",
"type": "wan",
"device": "cpu"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"39": {
"inputs": {
"vae_name": "wan_2.1_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"50": {
"inputs": {
"width": [
"64",
1
],
"height": [
"64",
2
],
"length": 121,
"batch_size": 1,
"positive": [
"6",
0
],
"negative": [
"7",
0
],
"vae": [
"39",
0
],
"start_image": [
"64",
0
]
},
"class_type": "WanImageToVideo",
"_meta": {
"title": "WanImageToVideo"
}
},
"52": {
"inputs": {
"image": "ComfyUI_00036_.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"54": {
"inputs": {
"shift": 8.000000000000002,
"model": [
"69",
0
]
},
"class_type": "ModelSamplingSD3",
"_meta": {
"title": "ModelSamplingSD3"
}
},
"55": {
"inputs": {
"shift": 8.000000000000002,
"model": [
"70",
0
]
},
"class_type": "ModelSamplingSD3",
"_meta": {
"title": "ModelSamplingSD3"
}
},
"57": {
"inputs": {
"add_noise": "enable",
"noise_seed": 375574453154296,
"steps": 6,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"start_at_step": 0,
"end_at_step": 3,
"return_with_leftover_noise": "enable",
"model": [
"54",
0
],
"positive": [
"50",
0
],
"negative": [
"50",
1
],
"latent_image": [
"50",
2
]
},
"class_type": "KSamplerAdvanced",
"_meta": {
"title": "KSampler (Advanced)"
}
},
"58": {
"inputs": {
"add_noise": "disable",
"noise_seed": 0,
"steps": 6,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"start_at_step": 3,
"end_at_step": 10000,
"return_with_leftover_noise": "disable",
"model": [
"55",
0
],
"positive": [
"50",
0
],
"negative": [
"50",
1
],
"latent_image": [
"57",
0
]
},
"class_type": "KSamplerAdvanced",
"_meta": {
"title": "KSampler (Advanced)"
}
},
"61": {
"inputs": {
"unet_name": "wan2.2_i2v_high_noise_14B_Q4_K_S.gguf"
},
"class_type": "UnetLoaderGGUF",
"_meta": {
"title": "Unet Loader (GGUF)"
}
},
"62": {
"inputs": {
"unet_name": "wan2.2_i2v_low_noise_14B_Q4_K_S.gguf"
},
"class_type": "UnetLoaderGGUF",
"_meta": {
"title": "Unet Loader (GGUF)"
}
},
"63": {
"inputs": {
"frame_rate": 32,
"loop_count": 0,
"filename_prefix": "wan22_",
"format": "video/h264-mp4",
"pix_fmt": "yuv420p",
"crf": 19,
"save_metadata": true,
"trim_to_audio": false,
"pingpong": false,
"save_output": true,
"images": [
"8",
0
]
},
"class_type": "VHS_VideoCombine",
"_meta": {
"title": "Video Combine 🎥🅥🅗🅢"
}
},
"64": {
"inputs": {
"width": 720,
"height": 1280,
"upscale_method": "lanczos",
"keep_proportion": "crop",
"pad_color": "0, 0, 0",
"crop_position": "center",
"divisible_by": 16,
"device": "cpu",
"image": [
"52",
0
]
},
"class_type": "ImageResizeKJv2",
"_meta": {
"title": "Resize Image v2"
}
},
"65": {
"inputs": {
"sage_attention": "sageattn_qk_int8_pv_fp8_cuda++",
"model": [
"61",
0
]
},
"class_type": "PathchSageAttentionKJ",
"_meta": {
"title": "Patch Sage Attention KJ"
}
},
"66": {
"inputs": {
"enable_fp16_accumulation": true,
"model": [
"65",
0
]
},
"class_type": "ModelPatchTorchSettings",
"_meta": {
"title": "Model Patch Torch Settings"
}
},
"67": {
"inputs": {
"sage_attention": "sageattn_qk_int8_pv_fp8_cuda++",
"model": [
"62",
0
]
},
"class_type": "PathchSageAttentionKJ",
"_meta": {
"title": "Patch Sage Attention KJ"
}
},
"68": {
"inputs": {
"enable_fp16_accumulation": true,
"model": [
"67",
0
]
},
"class_type": "ModelPatchTorchSettings",
"_meta": {
"title": "Model Patch Torch Settings"
}
},
"69": {
"inputs": {
"lora_name": "Wan21_I2V_14B_lightx2v_cfg_step_distill_lora_rank64.safetensors",
"strength_model": 3.0000000000000004,
"model": [
"66",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"70": {
"inputs": {
"lora_name": "Wan21_T2V_14B_lightx2v_cfg_step_distill_lora_rank64.safetensors",
"strength_model": 1.5000000000000002,
"model": [
"68",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoraLoaderModelOnly"
}
},
"75": {
"inputs": {
"model": "4xNomos2_otf_esrgan",
"precision": "fp16"
},
"class_type": "LoadUpscalerTensorrtModel",
"_meta": {
"title": "Load Upscale Tensorrt Model"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -0,0 +1,4 @@
{
"initialImage": "This image shows a girl with long hair sitting cross-legged in meditation on a colorful field that covers the curve of an Earth-like sphere. The vibrant ground is dotted with glowing hues of green, purple, and pink, resembling a dreamlike meadow. Behind her, radiant light shines, highlighting her silhouette and giving her a divine presence. Above, the vast cosmos unfolds—swirling nebulae in brilliant blues, pinks, and purples fill the sky, while planets and celestial spheres float gracefully in space. Below, layers of glowing clouds and reflective water mirror the cosmic colors. The atmosphere is mystical, serene, and transcendent, blending nature with the universe.",
"videoPrompt": "A girl with long hair sits cross-legged in meditation on a colorful glowing field, illuminated by soft radiant light. She remains completely still, serene, and peaceful.Camera is rotating slowly around her in right direction keep same distance. The surface of water is waving gently, reflecting the vibrant colors of the sky and the glowing field. The cosmic background with swirling nebulae, planets, and stars remains static, creating a mystical and tranquil atmosphere. The overall scene is ethereal and dreamlike, with a harmonious blend of nature and the universe."
}

View File

@ -0,0 +1,395 @@
import dotenv from 'dotenv';
import path from 'path';
import fs from 'fs/promises';
import { spawn } from 'child_process';
import { logger } from '../lib/logger';
import { generateImage } from '../lib/image-generator';
import { generateVideo } from '../lib/video-generator';
dotenv.config();
type Size = { width: number; height: number };
interface InfinitySceneConfig {
initialImage: string;
videoPrompt: string;
}
interface Server {
baseUrl?: string;
outputDir?: string;
inputDir?: string;
name: string;
}
const DEFAULT_SIZE: Size = { width: 720, height: 1280 };
const GENERATED_DIR = path.resolve('generated');
/**
* Load ComfyUI servers from env:
* - SERVER1_COMFY_BASE_URL, SERVER1_COMFY_OUTPUT_DIR
* - SERVER2_COMFY_BASE_URL, SERVER2_COMFY_OUTPUT_DIR
* (inputDir is inferred by replacing "output" with "input")
*/
function loadServers(): Server[] {
const servers: Server[] = [
{
name: 'SERVER1',
baseUrl: process.env.SERVER1_COMFY_BASE_URL,
outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR,
},
]
.filter((s) => !!s.baseUrl && !!s.outputDir)
.map((s) => ({
...s,
inputDir: s.outputDir!.replace(/output/i, 'input'),
}));
if (servers.length === 0) {
logger.warn('No servers configured. Please set SERVER{N}_COMFY_BASE_URL and SERVER{N}_COMFY_OUTPUT_DIR in .env');
} else {
for (const s of servers) {
logger.info(`Configured ${s.name}: baseUrl=${s.baseUrl}, outputDir=${s.outputDir}, inputDir=${s.inputDir}`);
}
}
return servers;
}
async function ensureDirs() {
await fs.mkdir(GENERATED_DIR, { recursive: true });
}
async function copyImageToAllServerInputs(servers: Server[], localGeneratedImagePath: string): Promise<string> {
const fileName = path.basename(localGeneratedImagePath);
for (const s of servers) {
if (!s.inputDir) continue;
const dest = path.join(s.inputDir, fileName);
try {
await fs.mkdir(s.inputDir, { recursive: true });
await fs.copyFile(localGeneratedImagePath, dest);
logger.debug(`Copied ${fileName} to ${s.name} input: ${dest}`);
} catch (err) {
logger.warn(`Failed to copy ${fileName} to ${s.name} input: ${err}`);
}
}
return fileName; // name used by Comfy workflow inputs
}
function pickServer(servers: Server[], idx: number): Server {
if (servers.length === 0) {
throw new Error('No servers configured.');
}
return servers[idx % servers.length];
}
async function fileExists(p: string): Promise<boolean> {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
/** ---------- ffmpeg / ffprobe helpers ---------- */
function runFfmpeg(args: string[], { cwd }: { cwd?: string } = {}): Promise<void> {
return new Promise((resolve, reject) => {
const proc = spawn('ffmpeg', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
let stderr = '';
proc.stdout.on('data', (d) => logger.debug(d.toString()));
proc.stderr.on('data', (d) => {
const msg = d.toString();
stderr += msg;
logger.debug(msg); // ffmpeg prints progress to stderr
});
proc.on('error', (err: any) => reject(err));
proc.on('close', (code) => {
if (code === 0) resolve();
else reject(new Error(`ffmpeg exited with code ${code}. ${stderr.slice(0, 1000)}`));
});
});
}
function runFfprobe(args: string[], { cwd }: { cwd?: string } = {}): Promise<{ stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
const proc = spawn('ffprobe', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
let stdout = '';
let stderr = '';
proc.stdout.on('data', (d) => (stdout += d.toString()));
proc.stderr.on('data', (d) => (stderr += d.toString()));
proc.on('error', (err) => reject(err));
proc.on('close', (code) => {
if (code === 0) resolve({ stdout, stderr });
else reject(new Error(`ffprobe exited with code ${code}. ${stderr.slice(0, 1000)}`));
});
});
}
/**
* Try to get precise total frame count using ffprobe.
* 1) nb_read_frames (requires -count_frames, may be slow but exact)
* 2) nb_frames (container-level, sometimes missing)
* Returns undefined if both unavailable.
*/
async function getVideoFrameCount(inputVideoPath: string): Promise<number | undefined> {
// Attempt 1: nb_read_frames
try {
const { stdout } = await runFfprobe([
'-v', 'error',
'-count_frames',
'-select_streams', 'v:0',
'-show_entries', 'stream=nb_read_frames',
'-of', 'default=nokey=1:noprint_wrappers=1',
inputVideoPath,
]);
const n = parseInt(stdout.trim(), 10);
if (Number.isFinite(n) && n > 0) return n;
} catch (e) {
logger.debug(`ffprobe nb_read_frames failed: ${e instanceof Error ? e.message : e}`);
}
// Attempt 2: nb_frames
try {
const { stdout } = await runFfprobe([
'-v', 'error',
'-select_streams', 'v:0',
'-show_entries', 'stream=nb_frames',
'-of', 'default=nokey=1:noprint_wrappers=1',
inputVideoPath,
]);
const n = parseInt(stdout.trim(), 10);
if (Number.isFinite(n) && n > 0) return n;
} catch (e) {
logger.debug(`ffprobe nb_frames failed: ${e instanceof Error ? e.message : e}`);
}
return undefined;
}
/**
* Fallback when frame count is unavailable:
* - Get duration (sec)
* - Seek extremely near the end with -sseof (negative seek from EOF)
* - Reverse that tiny tail and grab the first decoded frame
*/
async function extractLastFrameFallback(inputVideoPath: string, outputImagePath: string): Promise<void> {
await fs.mkdir(path.dirname(outputImagePath), { recursive: true });
// Read duration to decide a safe small tail (0.2s or 1% of duration)
let tail = 0.2;
try {
const { stdout } = await runFfprobe([
'-v', 'error',
'-select_streams', 'v:0',
'-show_entries', 'format=duration',
'-of', 'default=nokey=1:noprint_wrappers=1',
inputVideoPath,
]);
const dur = parseFloat(stdout.trim());
if (Number.isFinite(dur) && dur > 0) {
tail = Math.max(0.05, Math.min(0.5, dur * 0.01)); // 1% (min 0.05s, max 0.5s)
}
} catch { /* ignore */ }
const args = [
'-y',
'-sseof', `-${tail}`,
'-i', inputVideoPath,
'-vf', 'reverse',
'-vframes', '1',
'-q:v', '2',
outputImagePath,
];
await runFfmpeg(args);
}
/**
* Extract the last frame of a video into a PNG, accurately.
* Preferred: precise frame index with select=eq(n\,last)
* Fallback: tiny tail + reverse (handles weird containers/codecs)
*/
async function extractLastFrameAccurate(inputVideoPath: string, outputImagePath: string): Promise<void> {
const total = await getVideoFrameCount(inputVideoPath);
if (total && total > 0) {
const last = Math.max(0, total - 1);
await fs.mkdir(path.dirname(outputImagePath), { recursive: true });
const args = [
'-y',
'-i', inputVideoPath,
'-vf', `select=eq(n\\,${last})`,
'-vframes', '1',
'-q:v', '2',
outputImagePath,
];
try {
await runFfmpeg(args);
return;
} catch (e) {
logger.warn(`select by frame index failed, falling back: ${e instanceof Error ? e.message : e}`);
}
}
await extractLastFrameFallback(inputVideoPath, outputImagePath);
}
/**
* Concatenate two videos into a single MP4.
* First try concat demuxer with stream copy (fast, no re-encode).
* If it fails, fall back to re-encoding with concat filter.
*/
async function concatVideosFFmpeg(input1: string, input2: string, outputPath: string): Promise<void> {
await fs.mkdir(path.dirname(outputPath), { recursive: true });
// Try concat demuxer with copy (requires same codecs/params)
const listTxtPath = path.join(path.dirname(outputPath), `concat_${Date.now()}.txt`);
const listContent = `file '${path.resolve(input1).replace(/'/g, "'\\''")}'\nfile '${path.resolve(input2).replace(/'/g, "'\\''")}'\n`;
await fs.writeFile(listTxtPath, listContent, 'utf-8');
try {
await runFfmpeg(['-y', '-f', 'concat', '-safe', '0', '-i', listTxtPath, '-c', 'copy', outputPath]);
} catch (e) {
logger.warn(`Concat with -c copy failed, falling back to re-encode: ${e instanceof Error ? e.message : e}`);
// Fallback: re-encode with concat filter, video only
await runFfmpeg([
'-y',
'-i', input1,
'-i', input2,
'-filter_complex', 'concat=n=2:v=1:a=0',
'-c:v', 'libx264',
'-crf', '18',
'-preset', 'veryfast',
'-pix_fmt', 'yuv420p',
outputPath,
]);
} finally {
try { await fs.unlink(listTxtPath); } catch { /* ignore */ }
}
}
async function main() {
try {
await ensureDirs();
// Load scene config
const sceneRaw = await fs.readFile(path.resolve('src/infinityvideo_generator/scene.json'), 'utf-8');
const scene: InfinitySceneConfig = JSON.parse(sceneRaw);
const servers = loadServers();
if (servers.length === 0) {
return;
}
// Optional limiter (env MAX_LOOPS). If not set, loop infinitely.
const MAX_LOOPS = process.env.MAX_LOOPS ? Math.max(0, parseInt(process.env.MAX_LOOPS, 10)) : undefined;
// Session identifiers
const sessionId = Date.now().toString(36);
let rrIndex = 0; // round-robin index across servers
let iteration = 0;
// Step 2: Determine initial image (use existing file if provided)
const providedInitialImagePath = path.resolve('src/infinityvideo_generator/initial_image.png');
let initialImagePath: string;
let initialImageName: string;
if (await fileExists(providedInitialImagePath)) {
initialImagePath = providedInitialImagePath;
initialImageName = path.basename(initialImagePath);
logger.info(`Using existing initial image at ${initialImagePath}`);
} else {
initialImageName = `infinity_${sessionId}_i${iteration}.png`;
const serverForInitialImage = pickServer(servers, rrIndex++);
logger.info(`Generating initial image (${initialImageName}) on ${serverForInitialImage.name} (flux)...`);
initialImagePath = await generateImage(
scene.initialImage,
initialImageName,
serverForInitialImage.baseUrl!,
serverForInitialImage.outputDir!,
'flux',
DEFAULT_SIZE,
);
logger.info(`Initial image generated: ${initialImagePath}`);
}
// Step 3: Copy image to input folders and generate first video
const imageNameForComfy = await copyImageToAllServerInputs(servers, initialImagePath);
const firstVideoName = initialImageName.replace(/\.png$/i, `_v${iteration}.mp4`);
const serverForFirstVideo = pickServer(servers, rrIndex++);
logger.info(`Generating first video (${firstVideoName}) on ${serverForFirstVideo.name} using ${imageNameForComfy}...`);
// Use "light" workflow and short length similar to musicspot videos
let currentVideoPath = await generateVideo(
scene.videoPrompt,
imageNameForComfy,
firstVideoName,
serverForFirstVideo.baseUrl!,
serverForFirstVideo.outputDir!,
DEFAULT_SIZE,
true,
true
);
logger.info(`First video generated: ${currentVideoPath}`);
// Loop:
// 4) Extract last frame (accurate)
// 5) Generate a new video from that image
// 6) Concat current video + new video => becomes the new "current" video
while (true) {
if (MAX_LOOPS !== undefined && iteration >= MAX_LOOPS) {
logger.info(`Reached MAX_LOOPS=${MAX_LOOPS}. Stopping.`);
break;
}
iteration += 1;
const lastFrameName = `infinity_${sessionId}_lastframe_${iteration}.png`;
const lastFramePath = path.join(GENERATED_DIR, lastFrameName);
logger.info(`Extracting last frame (accurate) from ${currentVideoPath} -> ${lastFramePath}`);
await extractLastFrameAccurate(currentVideoPath, lastFramePath);
const lastFrameExists = await fileExists(lastFramePath);
if (!lastFrameExists) {
throw new Error(`Failed to extract last frame to ${lastFramePath}`);
}
// Copy to server inputs
const frameNameForComfy = await copyImageToAllServerInputs(servers, lastFramePath);
// Generate new video from last frame
const newVideoName = `infinity_${sessionId}_video_${iteration}.mp4`;
const serverForVideo = pickServer(servers, rrIndex++);
logger.info(`Generating new video (${newVideoName}) on ${serverForVideo.name} using ${frameNameForComfy}...`);
const newVideoPath = await generateVideo(
scene.videoPrompt,
frameNameForComfy,
newVideoName,
serverForVideo.baseUrl!,
serverForVideo.outputDir!,
DEFAULT_SIZE,
true,
true
);
logger.info(`New segment generated: ${newVideoPath}`);
// Concat current + new => new current
const concatenatedName = `infinity_${sessionId}_concat_${iteration}.mp4`;
const concatenatedPath = path.join(GENERATED_DIR, concatenatedName);
logger.info(`Concatenating videos: [${currentVideoPath}] + [${newVideoPath}] -> ${concatenatedPath}`);
await concatVideosFFmpeg(currentVideoPath, newVideoPath, concatenatedPath);
logger.info(`Concatenated video: ${concatenatedPath}`);
// Set as the new current video for next loop
currentVideoPath = concatenatedPath;
}
logger.info('Infinity video generation finished.');
} catch (err) {
logger.error('Fatal error in infinity video generator:', err);
}
}
main().catch((err) => {
logger.error('Unhandled error:', err);
});

View File

@ -0,0 +1,77 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
interface ImageSize {
width: number;
height: number;
}
async function generateImage(
prompt: string,
faceImage: string,
newFileName: string,
comfyBaseUrl: string,
comfyOutputDir: string,
size: ImageSize = { width: 720, height: 1280 }
): Promise<string> {
const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, '');
const COMFY_OUTPUT_DIR = comfyOutputDir;
let workflow;
workflow = JSON.parse(await fs.readFile('src/comfyworkflows/generate_image_with_face.json', 'utf-8'));
workflow['41']['inputs']['clip_l'] = prompt;
workflow['41']['inputs']['t5xxl'] = prompt;
// Set image name
workflow['49']['inputs']['image'] = faceImage;
// Set image name
//workflow['16']['inputs']['image'] = imageName2;
workflow['27']['inputs']['width'] = size.width;
workflow['27']['inputs']['height'] = size.height;
const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow });
const promptId = response.data.prompt_id;
let history;
do {
const historyResponse = await axios.get(`${COMFY_BASE_URL}/history/${promptId}`);
history = historyResponse.data[promptId];
} while (!history || Object.keys(history.outputs).length === 0);
const files = await fs.readdir(COMFY_OUTPUT_DIR!);
const generatedFiles = files.filter(file => file.startsWith('FACEIMAGE'));
const fileStats = await Promise.all(
generatedFiles.map(async (file) => {
const stat = await fs.stat(path.join(COMFY_OUTPUT_DIR!, file));
return { file, mtime: stat.mtime };
})
);
fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
const latestFile = fileStats[0].file;
const newFilePath = path.resolve('./generated', newFileName);
await fs.mkdir('./generated', { recursive: true });
const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile);
try {
await fs.unlink(newFilePath);
} catch (err) {
// ignore if not exists
}
await fs.copyFile(sourcePath, newFilePath);
return newFilePath;
}
export { generateImage };

View File

@ -16,16 +16,22 @@ async function generateVideo(
newFileName: string,
comfyBaseUrl: string,
comfyOutputDir: string,
size: VideoSize = { width: 720, height: 1280 }
size: VideoSize = { width: 720, height: 1280 },
isShort = false,
isLight = false
): Promise<string> {
const COMFY_BASE_URL = comfyBaseUrl.replace(/\/$/, '');
const COMFY_OUTPUT_DIR = comfyOutputDir;
const workflow = JSON.parse(await fs.readFile('src/comfyworkflows/generate_video.json', 'utf-8'));
const workflow = isLight ?
JSON.parse(await fs.readFile('src/comfyworkflows/generate_video_light.json', 'utf-8')) :
JSON.parse(await fs.readFile('src/comfyworkflows/generate_video.json', 'utf-8'));
workflow['6']['inputs']['text'] = prompt;
workflow['52']['inputs']['image'] = imagePath;
workflow['64']['inputs']['width'] = size.width;
workflow['64']['inputs']['height'] = size.height;
if (isShort) workflow['50']['inputs']['length'] = 89;
const response = await axios.post(`${COMFY_BASE_URL}/prompt`, { prompt: workflow });
const promptId = response.data.prompt_id;
@ -52,7 +58,7 @@ async function generateVideo(
const newFilePath = path.resolve('./generated', newFileName);
await fs.mkdir('./generated', { recursive: true });
const sourcePath = path.join(COMFY_OUTPUT_DIR!, latestFile);
await fs.copyFile(sourcePath, newFilePath);
//await fs.unlink(sourcePath);

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -0,0 +1,830 @@
{
"song": {
"title": "Crimson Shadows",
"artist": "Nocturna",
"genre": "Gothic Metal",
"mood": "Dark, powerful, dramatic"
},
"character": {
"bodyType": "slim yet strong presence",
"hairStyle": "long black hair with red streaks, slightly messy"
},
"scenes": [
{
"sceneId": 1,
"time": "Night",
"location": "Ballroom with broken chandeliers and candles",
"outfit": "white gothic ball gown with lace sleeves",
"cuts": [
{
"cutId": 1,
"pose": "standing tall in the center of the ballroom",
"action": "raising arms dramatically",
"camera": [
"full body shot with chandeliers above",
"low angle from cracked floor",
"overhead view of gown spreading",
"face zoom in on serious look",
"back shot showing gown trail"
]
},
{
"cutId": 2,
"pose": "leaning back scream",
"action": "shouting with wide-open mouth",
"camera": [
"face zoom in during scream",
"low angle emphasizing mouth open",
"side shot with candlelight shadows",
"zoom in to part of body trembling hand",
"overhead shot capturing scream posture"
]
},
{
"cutId": 3,
"pose": "walking slowly",
"action": "dragging fingers on curtain",
"camera": [
"back shot trailing gown",
"side shot of hand on velvet",
"full body shot from distance",
"face zoom in on haunted gaze",
"low angle with curtain shadows"
]
},
{
"cutId": 4,
"pose": "kneeling on broken tiles",
"action": "holding necklace tightly",
"camera": [
"overhead shot of kneeling pose",
"face zoom in with tears",
"low angle from shattered floor",
"side shot framing necklace",
"back shot with gown spread"
]
},
{
"cutId": 5,
"pose": "spinning mid-gown flare",
"action": "screaming with wide-open mouth mid-spin",
"camera": [
"face zoom in with hair flying",
"low angle capturing spin",
"overhead highlighting gown swirl",
"zoom in to mouth open",
"side shot with candle flames shaking"
]
}
]
},
{
"sceneId": 2,
"time": "Night",
"location": "Dark graveyard with blood moon",
"outfit": "red gothic dress with corset and veil",
"cuts": [
{
"cutId": 1,
"pose": "standing among gravestones",
"action": "lifting veil slowly",
"camera": [
"full body shot with gravestones",
"face zoom in through veil",
"overhead shot with moonlight",
"side shot with crosses behind",
"back shot with veil flowing"
]
},
{
"cutId": 2,
"pose": "bending forward toward the sky",
"action": "shouting with wide-open mouth at the blood moon",
"camera": [
"face zoom in from below",
"low angle with moon glow",
"overhead capturing scream to sky",
"side shot emphasizing mouth open",
"zoom in to part of body trembling hands"
]
},
{
"cutId": 3,
"pose": "kneeling by gravestone",
"action": "touching carved letters",
"camera": [
"back shot with gravestone silhouette",
"low angle with fog rising",
"face zoom in teary",
"overhead with candle accents",
"side shot of hand on stone"
]
},
{
"cutId": 4,
"pose": "walking past tombs",
"action": "dragging gown through dirt",
"camera": [
"full body wide shot",
"back shot with gown trail",
"overhead with drifting fog",
"side shot framing gravestones",
"zoom in to part of body—bleeding palm"
]
},
{
"cutId": 5,
"pose": "clutching chest in anguish",
"action": "screaming again with wide-open mouth",
"camera": [
"face zoom in intense",
"low angle shaky frame",
"overhead gown spread",
"side shot with blood moon glow",
"zoom in to part of body—trembling mouth"
]
}
]
},
{
"sceneId": 3,
"time": "Night",
"location": "Castle corridor with torches",
"outfit": "black velvet gothic gown with silver accents",
"cuts": [
{
"cutId": 1,
"pose": "walking slowly through corridor",
"action": "dragging hand across stone wall",
"camera": [
"back shot trailing behind",
"side shot hand scraping the wall",
"face zoom in haunted eyes",
"overhead torches flickering",
"full body wide frame"
]
},
{
"cutId": 2,
"pose": "standing between torches, head tilted back",
"action": "shouting with wide-open mouth",
"camera": [
"low angle torch flames",
"face zoom in mouth open",
"overhead capturing hall symmetry",
"side shot framing torches",
"zoom in to part of body—clenched fists"
]
},
{
"cutId": 3,
"pose": "kneeling near cracked stone",
"action": "holding broken goblet",
"camera": [
"overhead spilled goblet and shards",
"side shot with long shadow",
"face zoom in tragic expression",
"back shot of corridor depth",
"low angle torch flicker"
]
},
{
"cutId": 4,
"pose": "standing with hand raised",
"action": "reaching toward ceiling arches",
"camera": [
"full body shot angled upward",
"low angle showing height of arches",
"face zoom in determination",
"back shot framed by arch",
"overhead with soft torch glow"
]
},
{
"cutId": 5,
"pose": "arched back against wall",
"action": "screaming again with wide-open mouth",
"camera": [
"side shot hair flowing",
"face zoom in mouth open",
"low angle wall cracks",
"back shot dramatic shadow",
"overhead dim torch light"
]
}
]
},
{
"sceneId": 4,
"time": "Night",
"location": "Castle rooftop under stars and aurora",
"outfit": "red flowing gothic gown with crimson sash",
"cuts": [
{
"cutId": 1,
"pose": "standing on rooftop edge",
"action": "arms spread to the sky",
"camera": [
"full body shot with starscape",
"overhead aurora curtains",
"face zoom in serene gaze",
"back shot with gown streaming",
"low angle with night sky dome"
]
},
{
"cutId": 2,
"pose": "kneeling on cold stone",
"action": "shouting with wide-open mouth into the wind",
"camera": [
"face zoom in under stars",
"overhead aurora swirl around",
"side shot mouth open profile",
"low angle wind tugging fabric",
"zoom in to part of body—trembling hand on stone"
]
},
{
"cutId": 3,
"pose": "walking slowly along the ledge",
"action": "dragging gown across stone",
"camera": [
"back shot trailing fabric",
"side shot with aurora behind",
"face zoom in sorrow",
"overhead glinting stars",
"full body wide rooftop span"
]
},
{
"cutId": 4,
"pose": "sitting on the ledge",
"action": "staring at the blood moon",
"camera": [
"face zoom in pale moonlight",
"side shot moon edge glow",
"back shot open horizon",
"overhead framing gown spread",
"low angle subtle lens flare"
]
},
{
"cutId": 5,
"pose": "arched back with hair whipping",
"action": "screaming again with wide-open mouth",
"camera": [
"face zoom in intensity",
"side shot mouth open",
"low angle aurora painted sky",
"overhead moon halo",
"back shot wind-rushed hair"
]
}
]
},
{
"sceneId": 5,
"time": "Night",
"location": "Cathedral interior with neon red lamps",
"outfit": "white gothic lace dress with spiked choker",
"cuts": [
{
"cutId": 1,
"pose": "standing at altar",
"action": "raising arms slowly",
"camera": [
"full body altar frame",
"low angle stained glass glow",
"overhead neon lamps",
"face zoom in cold eyes",
"side shot candle rows"
]
},
{
"cutId": 2,
"pose": "in front of the altar",
"action": "shouting with wide-open mouth",
"camera": [
"face zoom in wide scream",
"side shot neon wash",
"low angle altar cross",
"overhead long shadows",
"zoom in to part of body—tensed throat"
]
},
{
"cutId": 3,
"pose": "kneeling in prayer",
"action": "clutching rosary tightly",
"camera": [
"back shot neon cross silhouette",
"face zoom in closed eyes",
"overhead subtle aura",
"side shot trembling fingers",
"low angle pew shadows"
]
},
{
"cutId": 4,
"pose": "walking through pews",
"action": "gown brushing wood",
"camera": [
"full body wide aisle",
"back shot trailing fabric",
"overhead shadow mosaic",
"face zoom in haunted look",
"side shot cracked pew ends"
]
},
{
"cutId": 5,
"pose": "kneeling backward",
"action": "screaming again with wide-open mouth",
"camera": [
"face zoom in strained",
"overhead neon cross glow",
"side shot mouth open profile",
"low angle altar frame",
"zoom in to part of body—open mouth"
]
}
]
},
{
"sceneId": 6,
"time": "Night",
"location": "Dungeon with chains and damp stone",
"outfit": "black gothic leather gown with lace gloves",
"cuts": [
{
"cutId": 1,
"pose": "standing near dangling chains",
"action": "touching cold iron",
"camera": [
"face zoom in fogged breath",
"side shot hand on chain",
"back shot against damp wall",
"low angle shadows of chains",
"overhead dim torch circle"
]
},
{
"cutId": 2,
"pose": "head thrown back",
"action": "shouting with wide-open mouth while gripping chains",
"camera": [
"face zoom in raw scream",
"side shot taut chains",
"low angle wall fissures",
"overhead claustrophobic frame",
"zoom in to part of body—mouth close"
]
},
{
"cutId": 3,
"pose": "kneeling, wrists lifted",
"action": "pulling against shackles",
"camera": [
"face zoom in wet lashes",
"side shot trembling wrists",
"back shot chain stretch",
"overhead square of light",
"low angle torch sputter"
]
},
{
"cutId": 4,
"pose": "standing and straining",
"action": "yanking chains from ring",
"camera": [
"full body struggle shot",
"side shot metal tension",
"back shot elongated shadow",
"face zoom in fury",
"overhead dust falling"
]
},
{
"cutId": 5,
"pose": "arched back, wrists high",
"action": "screaming again with wide-open mouth",
"camera": [
"face zoom in contorted scream",
"side shot mouth open",
"overhead chains rattling",
"low angle shadow clawing up wall",
"zoom in to part of body—throat close"
]
}
]
},
{
"sceneId": 7,
"time": "Morning",
"location": "Girly bedroom with dark lace curtains",
"outfit": "black pleated mini skirt, oversized white shirt, ripped stockings",
"cuts": [
{
"cutId": 1,
"pose": "sitting on bed cross-legged",
"action": "playing with necklace charm",
"camera": [
"full body shot on bed",
"side shot necklace in hand",
"face zoom in subtle smile",
"overhead quilt folds",
"back shot window light through lace"
]
},
{
"cutId": 2,
"pose": "leaning back over pillows",
"action": "shouting with wide-open mouth",
"camera": [
"face zoom in open mouth",
"side shot messy hair",
"low angle bed frame edge",
"overhead scream posture",
"zoom in to part of body—trembling hand"
]
},
{
"cutId": 3,
"pose": "lying sideways",
"action": "looking softly at camera",
"camera": [
"face zoom in gentle eyes",
"side shot pillow texture",
"overhead relaxed pose",
"back shot lace curtain glow",
"full body bed perspective"
]
},
{
"cutId": 4,
"pose": "standing by window",
"action": "touching glass",
"camera": [
"face zoom in window reflection",
"side shot pale fingers on glass",
"overhead curtain shadow pattern",
"back shot from doorway",
"low angle diffused light"
]
},
{
"cutId": 5,
"pose": "kneeling on bed",
"action": "screaming again with wide-open mouth",
"camera": [
"face zoom in scream",
"side shot mouth open profile",
"low angle trembling posture",
"overhead tangled sheets",
"zoom in to part of body—throat close"
]
}
]
},
{
"sceneId": 8,
"time": "Night",
"location": "Urban street with neon lights and wet pavement",
"outfit": "red tartan skirt, black crop top, fishnet stockings",
"cuts": [
{
"cutId": 1,
"pose": "walking on crosswalk",
"action": "flicking hair",
"camera": [
"full body wide crosswalk",
"side shot neon reflections",
"face zoom in smirk",
"back shot traffic lights",
"overhead city glow grid"
]
},
{
"cutId": 2,
"pose": "standing under neon sign",
"action": "shouting with wide-open mouth",
"camera": [
"face zoom in neon bloom",
"side shot mouth open",
"low angle billboard glow",
"overhead zebra stripes",
"zoom in to part of body—clenched fists"
]
},
{
"cutId": 3,
"pose": "leaning on graffiti wall",
"action": "crossing arms with attitude",
"camera": [
"side shot tag texture",
"face zoom in cool gaze",
"back shot city night bokeh",
"overhead shadow frame",
"full body casual stance"
]
},
{
"cutId": 4,
"pose": "kneeling on sidewalk",
"action": "tracing chalk graffiti",
"camera": [
"overhead chalk lines",
"side shot hand drawing",
"face zoom in focused",
"back shot wet pavement shimmer",
"low angle neon bleed"
]
},
{
"cutId": 5,
"pose": "jumping mid-beat",
"action": "screaming again with wide-open mouth in mid-air",
"camera": [
"face zoom in scream",
"side shot mouth open",
"overhead body frozen mid-jump",
"back shot neon flare",
"zoom in to part of body—throat close"
]
}
]
},
{
"sceneId": 9,
"time": "Afternoon",
"location": "Park bench under autumn trees",
"outfit": "black floral dress with lace stockings and boots",
"cuts": [
{
"cutId": 1,
"pose": "sitting on bench",
"action": "looking down thoughtfully",
"camera": [
"full body bench shot",
"side shot fallen leaves",
"face zoom in pensive gaze",
"overhead bench frame",
"back shot winding path"
]
},
{
"cutId": 2,
"pose": "standing in leaf fall",
"action": "shouting with wide-open mouth to the sky",
"camera": [
"face zoom in scream",
"side shot mouth open",
"low angle tree branches",
"overhead spinning leaves",
"zoom in to part of body—trembling hands"
]
},
{
"cutId": 3,
"pose": "lying on grass",
"action": "picking petals from a flower",
"camera": [
"overhead flower detail",
"face zoom in soft smile",
"side shot fingers on petals",
"back shot grassy field",
"full body relaxed frame"
]
},
{
"cutId": 4,
"pose": "walking along path",
"action": "hands in pockets",
"camera": [
"back shot autumn walkway",
"side shot casual stride",
"face zoom in subtle grin",
"overhead tree shadows",
"full body natural stance"
]
},
{
"cutId": 5,
"pose": "kneeling by the bench",
"action": "screaming again with wide-open mouth",
"camera": [
"face zoom in intensity",
"side shot mouth open profile",
"low angle roots and soil",
"overhead leaf carpet",
"zoom in to part of body—throat close"
]
}
]
},
{
"sceneId": 10,
"time": "Afternoon",
"location": "Trendy café corner with plants and soft lamps",
"outfit": "gothic casual: black skirt, lace top, choker",
"cuts": [
{
"cutId": 1,
"pose": "sitting at table",
"action": "smiling softly while stirring drink",
"camera": [
"full body cozy table shot",
"side shot spoon and cup",
"face zoom in gentle eyes",
"overhead latte foam swirl",
"back shot plant backdrop"
]
},
{
"cutId": 2,
"pose": "standing beside the table",
"action": "shouting with wide-open mouth",
"camera": [
"face zoom in bright lamps",
"side shot mouth open",
"low angle lamp glow",
"overhead table layout",
"zoom in to part of body—tensed throat"
]
},
{
"cutId": 3,
"pose": "writing in notebook",
"action": "biting lip thoughtfully",
"camera": [
"overhead page and pen",
"face zoom in focused eyes",
"side shot lace sleeve",
"back shot framed by chairs",
"full body seated posture"
]
},
{
"cutId": 4,
"pose": "sipping from cup",
"action": "looking away shyly",
"camera": [
"face zoom in eyes over rim",
"side shot cup touch",
"overhead saucer on table",
"back shot warm ambience",
"low angle lamp string bulbs"
]
},
{
"cutId": 5,
"pose": "kneeling on chair",
"action": "screaming again with wide-open mouth",
"camera": [
"face zoom in scream",
"side shot mouth open",
"low angle chair legs",
"overhead tabletop pattern",
"zoom in to part of body—throat close"
]
}
]
},
{
"sceneId": 11,
"time": "Evening",
"location": "City rooftop with skyline lights",
"outfit": "casual gothic: leather jacket, mini skirt, torn tights",
"cuts": [
{
"cutId": 1,
"pose": "standing on edge",
"action": "arms open to the wind",
"camera": [
"full body skyline silhouette",
"back shot city bokeh",
"side shot hair lifted",
"overhead roof lines",
"face zoom in calm power"
]
},
{
"cutId": 2,
"pose": "kneeling near the parapet",
"action": "shouting with wide-open mouth",
"camera": [
"face zoom in stormy scream",
"side shot mouth open",
"low angle city glow",
"overhead concrete texture",
"zoom in to part of body—clenched fists"
]
},
{
"cutId": 3,
"pose": "sitting on ledge",
"action": "looking down at traffic",
"camera": [
"back shot roads below",
"side shot dangling feet",
"face zoom in reflective",
"overhead ledge line",
"full body relaxed sit"
]
},
{
"cutId": 4,
"pose": "walking across roof",
"action": "wind blowing hair and jacket",
"camera": [
"full body wide stride",
"side shot jacket flutter",
"back shot trailing steps",
"overhead roof grid",
"face zoom in focused"
]
},
{
"cutId": 5,
"pose": "small jump on roof seam",
"action": "screaming again with wide-open mouth",
"camera": [
"face zoom in scream",
"side shot mouth open",
"overhead mid-air frame",
"low angle skyline towers",
"zoom in to part of body—throat close"
]
}
]
},
{
"sceneId": 12,
"time": "Night",
"location": "Underground subway platform with graffiti",
"outfit": "gothic streetwear: ripped jeans, black hoodie, silver chains",
"cuts": [
{
"cutId": 1,
"pose": "standing on platform",
"action": "hands in pockets, head tilted",
"camera": [
"full body trackside shot",
"side shot hoodie folds",
"face zoom in under tube light",
"overhead platform tiles",
"back shot tunnel perspective"
]
},
{
"cutId": 2,
"pose": "leaning toward the tunnel",
"action": "shouting with wide-open mouth",
"camera": [
"face zoom in echoing mouth",
"side shot mouth open",
"low angle rail shine",
"overhead yellow line",
"zoom in to part of body—tensed throat"
]
},
{
"cutId": 3,
"pose": "sitting on bench",
"action": "head tilted down over chains",
"camera": [
"back shot empty platform",
"side shot chain glint",
"face zoom in shadowed eyes",
"overhead bench geometry",
"full body moody sit"
]
},
{
"cutId": 4,
"pose": "walking along platform edge",
"action": "swinging arms slowly",
"camera": [
"full body long leading lines",
"side shot sneaker scuff",
"face zoom in composed",
"overhead fluorescent bands",
"back shot receding tunnel"
]
},
{
"cutId": 5,
"pose": "arched back near the pillar",
"action": "screaming again with wide-open mouth",
"camera": [
"face zoom in scream",
"side shot mouth open",
"low angle pillar grit",
"overhead tiled grid",
"zoom in to part of body—throat close"
]
}
]
}
]
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -0,0 +1,272 @@
{
"character": {
"bodyType": "slim and youthful",
"hairStyle": "long wavy hair with pastel blue and purple colors"
},
"scenes": [
{
"sceneId": 1,
"time": "Morning",
"location": "Cozy bedroom with pastel bedding and sunlight through curtains",
"outfit": "white oversized pajama shirt with thigh-high stockings",
"cuts": [
{
"cutId": 1,
"pose": "lying on bed with head resting on hands",
"action": "smiling softly at the camera",
"camera": [
"overhead shot from above capturing sunlight on her hair",
"close-up of her face with shallow depth of field",
"side angle showing body stretched on bed",
"slow zoom-in from doorway",
"handheld camera wobble for intimate feeling"
]
},
{
"cutId": 2,
"pose": "stretching arms above head while sitting on bed",
"action": "yawning cutely with eyes half closed",
"camera": [
"medium shot from foot of bed",
"low angle from floor emphasizing legs",
"wide shot with window light flaring",
"tracking shot circling around her stretch",
"soft focus tilt-shift framing"
]
},
{
"cutId": 3,
"pose": "kneeling on bed with playful look",
"action": "blowing a kiss to the camera",
"camera": [
"front close-up catching kiss in slow motion",
"wide shot with pastel background",
"camera tilt from below lips to eyes",
"360° pan around her kneeling pose",
"handheld push-in as she blows kiss"
]
}
]
},
{
"sceneId": 2,
"time": "Morning",
"location": "Bedroom near window with plants and sunlight",
"outfit": "light pastel camisole with short pleated skirt and stockings",
"cuts": [
{
"cutId": 1,
"pose": "sitting on window sill, legs slightly crossed",
"action": "looking outside then turning to smile",
"camera": [
"silhouette against sunlight",
"side profile with plants blurred in foreground",
"close-up of smile turning to camera",
"dolly-in from outside window glass",
"over-shoulder shot capturing view outside"
]
},
{
"cutId": 2,
"pose": "leaning against the wall, hands behind back",
"action": "giggling with shy expression",
"camera": [
"eye-level shot with wall texture visible",
"slight high angle to emphasize cuteness",
"medium close-up on giggle with tilt",
"rack focus between wall décor and her face",
"soft handheld sway left to right"
]
},
{
"cutId": 3,
"pose": "lying on floor with legs up against the wall",
"action": "kicking feet playfully while laughing",
"camera": [
"top-down view from ceiling",
"low angle from foot level",
"side shot capturing playful kicks",
"slow pan across her body",
"handheld camera zoom-in to laughter"
]
}
]
},
{
"sceneId": 3,
"time": "Afternoon",
"location": "Sunny street with pastel shops and flowers",
"outfit": "short pastel pink dress with white stockings and sneakers",
"cuts": [
{
"cutId": 1,
"pose": "standing with one hand on hip",
"action": "spinning gently in place",
"camera": [
"full body wide shot with pastel shops",
"low angle capturing dress swirl",
"tracking shot circling spin",
"handheld slow zoom-in to face",
"rear shot revealing spin from behind"
]
},
{
"cutId": 2,
"pose": "walking with small steps",
"action": "waving happily at the camera",
"camera": [
"tracking dolly shot in front",
"overhead drone shot of street",
"side follow shot at hip level",
"handheld jitter to mimic vlog",
"close-up on waving hand with face blurred"
]
},
{
"cutId": 3,
"pose": "leaning forward playfully",
"action": "making a heart shape with hands",
"camera": [
"tight close-up on hands forming heart",
"fish-eye wide close-up",
"side shot at 45°",
"POV shot as if receiving heart",
"zoom burst effect from wide to close"
]
}
]
},
{
"sceneId": 4,
"time": "Afternoon",
"location": "Trendy café with bright modern interior",
"outfit": "white blouse tucked into pleated skirt with stockings",
"cuts": [
{
"cutId": 1,
"pose": "sitting at a table with chin on hands",
"action": "smiling softly while tilting head",
"camera": [
"front eye-level close-up",
"soft focus on eyes, blurred background",
"over-shoulder shot of coffee cup",
"panning shot across table to her",
"low angle from table surface"
]
},
{
"cutId": 2,
"pose": "standing by window with crossed legs",
"action": "writing playfully in a small notebook",
"camera": [
"profile with sunlight flare",
"close-up of pen on paper then tilt to face",
"tracking shot from feet to head",
"reflection in window glass",
"medium shot with bokeh lights behind"
]
},
{
"cutId": 3,
"pose": "sitting sideways on chair with one leg up",
"action": "taking a sip from a cup with cute expression",
"camera": [
"tight focus on lips touching cup",
"medium shot framed by chair back",
"slight dutch angle for energy",
"slow dolly-in on playful sip",
"wide establishing shot of café"
]
}
]
},
{
"sceneId": 5,
"time": "Night",
"location": "Elegant ballroom with chandeliers",
"outfit": "sparkly silver mini dress with black stockings and heels",
"cuts": [
{
"cutId": 1,
"pose": "standing tall with one hand on waist",
"action": "turning slowly while smiling confidently",
"camera": [
"wide shot capturing chandelier",
"low angle emphasizing elegance",
"tracking dolly rotation",
"close-up of smile during turn",
"rear tracking shot revealing gown shimmer"
]
},
{
"cutId": 2,
"pose": "sitting gracefully on a velvet chair",
"action": "crossing legs elegantly",
"camera": [
"medium close-up from side",
"top-down angle showing chair texture",
"front focus on crossed legs",
"soft rack focus from chair to her face",
"panning shot circling chair"
]
},
{
"cutId": 3,
"pose": "leaning on railing with dreamy look",
"action": "gazing at the chandelier lights",
"camera": [
"over-shoulder shot of chandelier view",
"profile silhouette with golden backlight",
"wide shot capturing ballroom depth",
"handheld tilt up from railing to face",
"slow dolly-out revealing emptiness of hall"
]
}
]
},
{
"sceneId": 6,
"time": "Night",
"location": "Luxurious bedroom with soft golden lighting",
"outfit": "black lace camisole with mini skirt and stockings",
"cuts": [
{
"cutId": 1,
"pose": "lying sideways on bed with legs slightly bent",
"action": "looking at the camera with sultry eyes",
"camera": [
"close-up on eyes with blurred background",
"tracking shot along legs up to face",
"overhead soft focus",
"low angle from bed surface",
"handheld intimate pan across body"
]
},
{
"cutId": 2,
"pose": "sitting at vanity table",
"action": "putting on earrings slowly",
"camera": [
"mirror reflection focus",
"close-up on hands adjusting earrings",
"profile side shot with warm glow",
"over-shoulder shot including vanity lights",
"slow push-in from doorway"
]
},
{
"cutId": 3,
"pose": "kneeling on bed with arched back",
"action": "running hand through hair with sensual expression",
"camera": [
"rear shot with back arch emphasized",
"medium close-up focusing on hair movement",
"low angle capturing curves",
"soft focus candlelight bokeh",
"circling dolly shot for dramatic effect"
]
}
]
}
]
}

Binary file not shown.

View File

@ -0,0 +1,240 @@
import dotenv from 'dotenv';
import path from 'path';
import fs from 'fs/promises';
import { logger } from '../lib/logger';
import { callOpenAI } from '../lib/openai';
import { generateImage as generateFaceImage } from '../lib/image-generator-face';
dotenv.config();
type Size = { width: number; height: number };
interface MusicSpotCharacter {
bodyType: string;
hairStyle: string;
}
interface MusicSpotCut {
cutId: number;
pose: string;
action: string;
camera?: string[]; // list of camera variants per cut
}
interface MusicSpotScene {
sceneId: number;
time: string;
location: string;
outfit: string;
cuts: MusicSpotCut[];
}
interface MusicSpotConfig {
character: MusicSpotCharacter;
scenes: MusicSpotScene[];
}
interface Server {
baseUrl?: string;
outputDir?: string;
inputDir?: string;
name: string;
}
const DEFAULT_SIZE: Size = { width: 720, height: 1280 };
const FOLDER = process.argv[2] || process.env.MUSICSPOT_FOLDER || 'oputstise';
const FOLDER_SAFE = FOLDER.replace(/[/\\?%*:|"<>]/g, '_');
const FACE_SRC = path.resolve(`src/musicspot_generator/${FOLDER}/face.png`);
const GENERATED_DIR = path.resolve('generated');
function loadServers(): Server[] {
const servers: Server[] = [
{
name: 'SERVER1',
baseUrl: process.env.SERVER1_COMFY_BASE_URL,
outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR,
},
/*
{
name: 'SERVER2',
baseUrl: process.env.SERVER2_COMFY_BASE_URL,
outputDir: process.env.SERVER2_COMFY_OUTPUT_DIR,
}, */
]
.filter((s) => !!s.baseUrl && !!s.outputDir)
.map((s) => ({
...s,
inputDir: s.outputDir!.replace(/output/i, 'input'),
}));
if (servers.length === 0) {
logger.warn('No servers configured. Please set SERVER{N}_COMFY_BASE_URL and SERVER{N}_COMFY_OUTPUT_DIR in .env');
} else {
for (const s of servers) {
logger.info(`Configured ${s.name}: baseUrl=${s.baseUrl}, outputDir=${s.outputDir}, inputDir=${s.inputDir}`);
}
}
return servers;
}
async function ensureDirs() {
await fs.mkdir(GENERATED_DIR, { recursive: true });
}
async function copyFaceToServers(servers: Server[]): Promise<string> {
const faceFileName = 'face.png';
// Validate face source
try {
await fs.access(FACE_SRC);
} catch {
throw new Error(`Face image not found at ${FACE_SRC}`);
}
for (const srv of servers) {
if (!srv.inputDir) continue;
const dest = path.join(srv.inputDir, faceFileName);
try {
await fs.mkdir(srv.inputDir, { recursive: true });
await fs.copyFile(FACE_SRC, dest);
logger.info(`Copied face image to ${srv.name} input: ${dest}`);
} catch (err) {
logger.warn(`Failed to copy face image to ${srv.name}: ${err}`);
}
}
return faceFileName; // Comfy expects file name present in input dir
}
function buildImagePromptRequest(
character: MusicSpotCharacter,
scene: MusicSpotScene,
cut: MusicSpotCut,
cameraIntent: string
): string {
return `
Return exactly one JSON object, nothing else: { "imagePrompt": "Cinematic realistic photo, (camera framing),(character),(pose),(time),(location),(outfit),(action),(lighting)" }.
Write "imagePrompt" in around 110140 words to generate a still portrait image (720x1280 vertical).
Keep a consistent character identity using the provided face image (identity preservation), but do not mention any camera brand/model.
Describe clearly and concretely:
- Character: ${character.bodyType}; hair: ${character.hairStyle}
- Camera framing/composition intention: ${cameraIntent}
- Time: ${scene.time}
- Location: ${scene.location}
- Outfit: ${scene.outfit}
- Pose: ${cut.pose}
- Action/Expression: ${cut.action}
- Lighting: please be creative and make beautiful lighting, I like something like luminous, colorful
Only respond with JSON.
`.trim();
}
async function getImagePromptFromOpenAI(req: string): Promise<string> {
const res = await callOpenAI(req);
const prompt = res?.imagePrompt || res?.image_prompt || res?.prompt;
if (!prompt || typeof prompt !== 'string') {
throw new Error('OpenAI failed to return imagePrompt JSON.');
}
return prompt.trim();
}
function pickServer(servers: Server[], idx: number): Server {
if (servers.length === 0) {
throw new Error('No servers configured.');
}
return servers[idx % servers.length];
}
async function main() {
try {
await ensureDirs();
// Load scenes.json
const configRaw = await fs.readFile(path.resolve(`src/musicspot_generator/${FOLDER}/scenes.json`), 'utf-8');
const cfg: MusicSpotConfig = JSON.parse(configRaw);
const servers = loadServers();
if (servers.length === 0) {
return;
}
// Ensure face.png in each server's input
const faceFileName = await copyFaceToServers(servers);
// Generate images only (no video here). Intended to be run first.
let imageTaskIndex = 0;
for (const scene of cfg.scenes) {
logger.info(`=== Scene ${scene.sceneId}: Image generation start ===`);
for (const cut of scene.cuts) {
const cameraVariants =
Array.isArray(cut.camera) && cut.camera.length > 0
? cut.camera
: ['eye-level medium shot', 'slight left 30°', 'slight right 30°', 'slight high angle', 'slight low angle'];
for (let camIdx = 0; camIdx < cameraVariants.length; camIdx++) {
const cameraIntent = cameraVariants[camIdx];
const variantIndex = camIdx + 1;
const imgFileName = `${FOLDER_SAFE}_musicspot_s${scene.sceneId}_c${cut.cutId}_v${variantIndex}.png`;
const outputPath = path.join(GENERATED_DIR, imgFileName);
// Skip generation if target file already exists
try {
await fs.access(outputPath);
logger.info(`Skipping generation, file already exists: ${outputPath}`);
continue;
} catch {
// File does not exist; proceed with generation
}
// 1) Generate image prompt for this camera
logger.info(`Scene ${scene.sceneId} - Cut ${cut.cutId} - Cam${variantIndex}: generating image prompt...`);
const imgPromptReq = buildImagePromptRequest(cfg.character, scene, cut, cameraIntent);
let imagePrompt: string;
try {
imagePrompt = await getImagePromptFromOpenAI(imgPromptReq);
} catch (err) {
logger.error(
`OpenAI image prompt failed for scene ${scene.sceneId} cut ${cut.cutId} cam ${variantIndex}: ${err}`
);
continue;
}
// 2) Generate one image using face conditioning for this specific camera
const serverForImage = pickServer(servers, imageTaskIndex++);
logger.info(`Generating image (${imgFileName}) on ${serverForImage.name}...`);
try {
// Use only the face file name for the workflow image input (Comfy expects it in its input dir)
const finalImagePath = await generateFaceImage(
`Realistic photo, ultra detailed, high contrast, ${imagePrompt}`,
faceFileName,
imgFileName,
serverForImage.baseUrl!,
serverForImage.outputDir!,
DEFAULT_SIZE
);
logger.info(`Image generated: ${finalImagePath}`);
} catch (err) {
logger.error(`Image generation failed (${imgFileName}) on ${serverForImage.name}: ${err}`);
continue;
}
}
}
logger.info(`=== Scene ${scene.sceneId}: Image generation complete ===`);
}
logger.info('Image generation for all scenes completed.');
} catch (err) {
logger.error('Fatal error in music spot image generator:', err);
}
}
main().catch((err) => {
logger.error('Unhandled error:', err);
});

View File

@ -0,0 +1,366 @@
import dotenv from 'dotenv';
import path from 'path';
import fs from 'fs/promises';
import { logger } from '../lib/logger';
import { callOpenAI } from '../lib/openai';
import { generateImage as generateFaceImage } from '../lib/image-generator-face';
import { generateVideo } from '../lib/video-generator';
dotenv.config();
type Size = { width: number; height: number };
interface MusicSpotCharacter {
bodyType: string;
hairStyle: string;
}
interface MusicSpotCut {
cutId: number;
pose: string;
action: string;
camera?: string[]; // list of 5 camera variants per cut
}
interface MusicSpotScene {
sceneId: number;
time: string;
location: string;
outfit: string;
cuts: MusicSpotCut[];
}
interface MusicSpotConfig {
character: MusicSpotCharacter;
scenes: MusicSpotScene[];
}
interface Server {
baseUrl?: string;
outputDir?: string;
inputDir?: string;
name: string;
}
const DEFAULT_SIZE: Size = { width: 720, height: 1280 };
const FACE_SRC = path.resolve('src/musicspot_generator/face.png');
const GENERATED_DIR = path.resolve('generated');
function loadServers(): Server[] {
const servers: Server[] = [
{
name: 'SERVER1',
baseUrl: process.env.SERVER1_COMFY_BASE_URL,
outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR,
}
/*{
name: 'SERVER2',
baseUrl: process.env.SERVER2_COMFY_BASE_URL,
outputDir: process.env.SERVER2_COMFY_OUTPUT_DIR,
}*/,
]
.filter(s => !!s.baseUrl && !!s.outputDir)
.map(s => ({
...s,
// Convert output dir to input dir by convention
inputDir: s.outputDir!.replace(/output/i, 'input'),
}));
if (servers.length === 0) {
logger.warn('No servers configured. Please set SERVER{N}_COMFY_BASE_URL and SERVER{N}_COMFY_OUTPUT_DIR in .env');
} else {
for (const s of servers) {
logger.info(`Configured ${s.name}: baseUrl=${s.baseUrl}, outputDir=${s.outputDir}, inputDir=${s.inputDir}`);
}
}
return servers;
}
async function ensureDirs() {
await fs.mkdir(GENERATED_DIR, { recursive: true });
}
async function copyFaceToServers(servers: Server[]): Promise<{ fileName: string; absPerServer: Record<string, string> }> {
const faceFileName = 'face.png';
const absPerServer: Record<string, string> = {};
// Validate face source
try {
await fs.access(FACE_SRC);
} catch {
throw new Error(`Face image not found at ${FACE_SRC}`);
}
for (const srv of servers) {
if (!srv.inputDir) continue;
const dest = path.join(srv.inputDir, faceFileName);
try {
await fs.mkdir(srv.inputDir, { recursive: true });
await fs.copyFile(FACE_SRC, dest);
absPerServer[srv.name] = path.resolve(dest);
logger.info(`Copied face image to ${srv.name} input: ${dest}`);
} catch (err) {
logger.warn(`Failed to copy face image to ${srv.name}: ${err}`);
}
}
return { fileName: faceFileName, absPerServer };
}
function buildImagePromptRequest(
character: MusicSpotCharacter,
scene: MusicSpotScene,
cut: MusicSpotCut,
cameraIntent: string
): string {
// Ask OpenAI to return JSON: { "imagePrompt": "..." }
return `
Return exactly one JSON object, nothing else: { "imagePrompt": "..." }.
Write "imagePrompt" in around 110140 words to generate a still portrait image (720x1280 vertical).
Keep a consistent character identity using the provided face image (identity preservation), but do not mention any camera brand/model.
Describe clearly and concretely:
- Character: ${character.bodyType}; hair: ${character.hairStyle}
- Time: ${scene.time}
- Location: ${scene.location}
- Outfit: ${scene.outfit}
- Pose: ${cut.pose}
- Action/Expression: ${cut.action}
- Camera framing/composition intention: ${cameraIntent}
- Lighting/mood/style: cohesive and realistic, natural skin tones, soft depth of field.
Avoid: brand names, copyrighted characters, extreme or explicit content, text overlays, watermarks, multiple people. Focus on a single subject medium-full portrait, tasteful and aesthetic. Use simple sentences.
Only respond with JSON.
`.trim();
}
function buildVideoPromptRequest(
character: MusicSpotCharacter,
scene: MusicSpotScene,
cut: MusicSpotCut,
cameraIntent: string
): string {
// Ask OpenAI to return JSON: { "videoPrompt": "..." }
// Strong constraints to avoid "cut/zoom" etc. Keep a single continuous 8s shot.
return `
Return exactly one JSON object and nothing else: { "videoPrompt": "..." }.
Write "videoPrompt" in 100140 words. Present tense. Concrete, simple sentences.
HARD RULES:
- One continuous 8-second shot (oner). No edits.
- Fixed location and general vantage; maintain spatial continuity.
- No zooms, no rack zoom, no smash/push-in, no cuts, no transitions, no "meanwhile".
- Camera motion: at most a slight pan/tilt or subtle dolly within 1 meter.
- Keep framing consistent (vertical 720x1280). Avoid technical brand names or lens jargon.
Incorporate the following camera intention: "${cameraIntent}".
If it conflicts with HARD RULES (e.g., zoom, push-in, extreme moves), reinterpret it into a subtle, compliant motion (e.g., gentle glide, slight pan/tilt) while preserving the creative intent.
Describe:
1) Main action: ${cut.action}
2) Pose/composition: ${cut.pose}
3) Scene/time/location/outfit: ${scene.time}; ${scene.location}; outfit: ${scene.outfit}
4) Lighting/mood/style coherent with the character: ${character.bodyType}; hair: ${character.hairStyle}
Prohibited (case-insensitive): cut, cuts, cutting, quick cut, insert, close-up, extreme close-up, zoom, zooming, push-in, pull-out, whip, switch angle, change angle, montage, cross-cut, smash cut, transition, meanwhile, later.
Only respond with JSON.
`.trim();
}
async function getImagePromptFromOpenAI(req: string): Promise<string> {
const res = await callOpenAI(req);
const prompt = res?.imagePrompt || res?.image_prompt || res?.prompt;
if (!prompt || typeof prompt !== 'string') {
throw new Error('OpenAI failed to return imagePrompt JSON.');
}
return prompt.trim();
}
async function getVideoPromptFromOpenAI(req: string): Promise<string> {
const res = await callOpenAI(req);
const prompt = res?.videoPrompt || res?.video_prompt || res?.prompt;
if (!prompt || typeof prompt !== 'string') {
throw new Error('OpenAI failed to return videoPrompt JSON.');
}
return prompt.trim();
}
function pickServer(servers: Server[], idx: number): Server {
if (servers.length === 0) {
throw new Error('No servers configured.');
}
return servers[idx % servers.length];
}
async function copyImageToAllServerInputs(servers: Server[], localGeneratedImagePath: string): Promise<string> {
const fileName = path.basename(localGeneratedImagePath);
const copies: string[] = [];
for (const s of servers) {
if (!s.inputDir) continue;
const dest = path.join(s.inputDir, fileName);
try {
await fs.copyFile(localGeneratedImagePath, dest);
copies.push(dest);
logger.debug(`Copied ${fileName} to ${s.name} input: ${dest}`);
} catch (err) {
logger.warn(`Failed to copy ${fileName} to ${s.name} input: ${err}`);
}
}
return fileName; // return the name used for Comfy workflows
}
async function main() {
try {
await ensureDirs();
// Load scenes.json
const configRaw = await fs.readFile(path.resolve('src/musicspot_generator/scenes.json'), 'utf-8');
const cfg: MusicSpotConfig = JSON.parse(configRaw);
const servers = loadServers();
if (servers.length === 0) {
return;
}
// Ensure face.png in each server's input
const { fileName: faceFileName } = await copyFaceToServers(servers);
// Two-phase per scene:
// Phase 1: Generate ALL images for the scene (to keep image model warm)
// Phase 2: Generate ALL videos for the images of that scene (to keep video model warm)
let imageTaskIndex = 0;
let videoTaskIndex = 0;
for (const scene of cfg.scenes) {
logger.info(`=== Scene ${scene.sceneId}: Phase 1 - Image generation start ===`);
type SceneImageItem = {
sceneId: number;
cutId: number;
cameraIntent: string;
imgFileName: string;
imgPath: string;
};
const sceneImageItems: SceneImageItem[] = [];
// Phase 1: images
for (const cut of scene.cuts) {
const cameraVariants = Array.isArray(cut.camera) && cut.camera.length > 0
? cut.camera
: ['eye-level medium shot', 'slight left 30°', 'slight right 30°', 'slight high angle', 'slight low angle'];
for (let camIdx = 0; camIdx < cameraVariants.length; camIdx++) {
const cameraIntent = cameraVariants[camIdx];
const variantIndex = camIdx + 1;
// 1) Generate image prompt for this camera
logger.info(`Scene ${scene.sceneId} - Cut ${cut.cutId} - Cam${variantIndex}: generating image prompt...`);
const imgPromptReq = buildImagePromptRequest(cfg.character, scene, cut, cameraIntent);
let imagePrompt: string;
try {
imagePrompt = await getImagePromptFromOpenAI(imgPromptReq);
} catch (err) {
logger.error(`OpenAI image prompt failed for scene ${scene.sceneId} cut ${cut.cutId} cam ${variantIndex}: ${err}`);
continue;
}
// 2) Generate one image using face conditioning for this specific camera
const serverForImage = pickServer(servers, imageTaskIndex++);
const imgFileName = `musicspot_s${scene.sceneId}_c${cut.cutId}_v${variantIndex}.png`;
logger.info(`Generating image (${imgFileName}) on ${serverForImage.name}...`);
try {
// Use only the face file name for the workflow image input (Comfy expects it in its input dir)
const finalImagePath = await generateFaceImage(
imagePrompt,
faceFileName,
imgFileName,
serverForImage.baseUrl!,
serverForImage.outputDir!,
DEFAULT_SIZE
);
logger.info(`Image generated: ${finalImagePath}`);
sceneImageItems.push({
sceneId: scene.sceneId,
cutId: cut.cutId,
cameraIntent,
imgFileName,
imgPath: finalImagePath,
});
} catch (err) {
logger.error(`Image generation failed (${imgFileName}) on ${serverForImage.name}: ${err}`);
continue;
}
}
}
logger.info(`=== Scene ${scene.sceneId}: Phase 1 complete. Generated ${sceneImageItems.length} image(s). ===`);
logger.info(`=== Scene ${scene.sceneId}: Phase 2 - Video generation start ===`);
// Phase 2: videos for all images generated in this scene
for (const item of sceneImageItems) {
// Find original cut info for prompts
const cut = scene.cuts.find(c => c.cutId === item.cutId);
if (!cut) {
logger.warn(`Cut ${item.cutId} not found for scene ${scene.sceneId}, skipping video.`);
continue;
}
// 3) Generate video prompt for this camera
logger.info(`Generating video prompt for ${item.imgFileName} (scene ${scene.sceneId}, cut ${item.cutId})...`);
const vidPromptReq = buildVideoPromptRequest(cfg.character, scene, cut, item.cameraIntent);
let videoPrompt: string;
try {
videoPrompt = await getVideoPromptFromOpenAI(vidPromptReq);
} catch (err) {
logger.error(`OpenAI video prompt failed for ${item.imgFileName}: ${err}`);
continue;
}
// 4) Copy the base image to every server's input folder
const imageFileNameForComfy = await copyImageToAllServerInputs(servers, item.imgPath);
// 5) Generate video on a chosen server (round-robin)
const serverForVideo = pickServer(servers, videoTaskIndex++);
const videoFileName = item.imgFileName.replace(/\.png$/i, '.mp4');
logger.info(`Generating video (${videoFileName}) on ${serverForVideo.name} using ${imageFileNameForComfy}...`);
try {
const videoPath = await generateVideo(
videoPrompt,
imageFileNameForComfy,
videoFileName,
serverForVideo.baseUrl!,
serverForVideo.outputDir!,
DEFAULT_SIZE
);
logger.info(`Video generated: ${videoPath}`);
} catch (err) {
logger.error(`Video generation failed (${videoFileName}) on ${serverForVideo.name}: ${err}`);
}
}
logger.info(`=== Scene ${scene.sceneId}: Phase 2 complete. ===`);
}
logger.info('Music spot generation completed.');
} catch (err) {
logger.error('Fatal error in music spot generator:', err);
} finally {
// Optional: do not exit to allow long-running processes; but align with other scripts:
// process.exit();
}
}
main().catch(err => {
logger.error('Unhandled error:', err);
// process.exit(1);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -0,0 +1,827 @@
{
"character": {
"bodyType": "slim and youthful",
"hairStyle": "long straight blonde hair, white skin, blue eyes, light makeup"
},
"scenes": [
{
"sceneId": 1,
"time": "Evening",
"location": "Recording studio with warm lighting, microphone, pop filter, and soundproof walls",
"outfit": "cute pastel dress with stockings and studio-style headphones",
"cuts": [
{
"cutId": 1,
"pose": "standing close to microphone with both hands on headphones",
"action": "singing passionately with eyes closed",
"camera": [
"front medium shot centered on microphone and pop filter",
"close-up on her lips behind pop filter",
"side profile with blurred studio lights",
"low angle capturing hands holding headphones",
"over-shoulder shot showing recording booth perspective"
]
},
{
"cutId": 2,
"pose": "leaning slightly forward into microphone",
"action": "singing softly with focused expression",
"camera": [
"tight close-up through pop filter mesh",
"side close-up emphasizing focus in her eyes",
"rear shot with blurred soundboard in background",
"medium shot with studio monitors visible",
"handheld dolly-in for intimate effect"
]
},
{
"cutId": 3,
"pose": "one hand holding microphone stand, other hand raised slightly",
"action": "singing energetically with emotion",
"camera": [
"front low angle emphasizing raised hand",
"wide shot showing booth environment",
"profile shot with dramatic lighting",
"close-up on hand gripping mic stand",
"rear shot with blurred pop filter frame"
]
},
{
"cutId": 4,
"pose": "adjusting headphones with one hand while singing",
"action": "smiling softly between lyrics",
"camera": [
"front close-up on headphones adjustment",
"side profile smile with blurred mic",
"tight close-up on eyes sparkling",
"rear dolly shot focusing on headphones",
"medium shot with warm studio lights behind"
]
},
{
"cutId": 5,
"pose": "sitting on studio stool in front of mic",
"action": "closing eyes and holding lyrics sheet while singing",
"camera": [
"front shot framing mic and lyric sheet",
"close-up on lyric sheet in hand",
"side shot capturing relaxed pose",
"rear shot with blurred mixing desk",
"wide establishing shot of whole studio booth"
]
}
]
},
{
"sceneId": 2,
"time": "Day",
"location": "Minimalistic modern recording studio, all white walls and furniture, microphone with pop filter and sound panels",
"outfit": "elegant pure white dress with sheer stockings and studio headphones",
"cuts": [
{
"cutId": 1,
"pose": "standing gracefully in front of white microphone setup",
"action": "singing with serene expression, both hands gently on headphones",
"camera": [
"front medium shot with pure white background",
"close-up on serene face framed by white headphones",
"side profile with white mic and pop filter",
"low angle emphasizing elegance of white dress",
"rear over-shoulder shot into white booth"
]
},
{
"cutId": 2,
"pose": "leaning slightly toward microphone stand",
"action": "singing softly with eyes closed",
"camera": [
"tight close-up through white pop filter mesh",
"profile capturing closed eyes and focus",
"rear shot showing clean white acoustic panels",
"medium shot with glowing white lighting",
"slow dolly-in on her face from side"
]
},
{
"cutId": 3,
"pose": "raising one hand gracefully in the air while singing",
"action": "expressing emotion with body movement",
"camera": [
"front low angle emphasizing raised hand",
"wide shot of white studio environment",
"side profile with flowing dress detail",
"close-up on extended hand in soft light",
"rear shot capturing silhouette in white glow"
]
},
{
"cutId": 4,
"pose": "adjusting headphones gently with one hand",
"action": "smiling subtly between lyrics",
"camera": [
"front close-up with headphones in frame",
"side profile smile against white wall",
"tight close-up on sparkling eyes",
"medium shot with microphone blurred in foreground",
"over-shoulder capturing white minimalist interior"
]
},
{
"cutId": 5,
"pose": "sitting elegantly on modern white stool",
"action": "holding lyric sheet in lap while singing softly",
"camera": [
"front shot framing mic and lyric sheet",
"close-up on hands holding paper",
"side shot with white studio lights glowing",
"rear shot showing full white booth setup",
"wide establishing shot of minimal white studio"
]
}
]
},
{
"sceneId": 3,
"time": "Afternoon",
"location": "Spacious dance studio with mirrored walls, wooden floor, and bright ceiling lights",
"outfit": "sporty outfit: cropped pastel hoodie with black track pants and white sneakers",
"appearance": "hair wet and clinging to face from sweat, skin glowing with perspiration",
"cuts": [
{
"cutId": 1,
"pose": "standing in front of mirror with hands on knees",
"action": "breathing heavily, sweat face, wet hair",
"camera": [
"front medium shot showing sweat on face",
"rear shot capturing reflection in mirror",
"low angle emphasizing strong posture",
"side close-up on sweat dripping",
"handheld shaky cam for intensity"
]
},
{
"cutId": 2,
"pose": "leaning back slightly with arms raised",
"action": "stretching arms after intense dancing",
"camera": [
"front wide shot with arms extended",
"side profile with sweat-soaked hair",
"low angle highlighting arms and torso",
"rear shot with mirrored reflection",
"close-up on exhausted but determined face"
]
},
{
"cutId": 3,
"pose": "mid-dance move, sliding one foot forward",
"action": "serious expression, body low to ground",
"camera": [
"front wide shot capturing dynamic motion",
"low angle emphasizing movement power",
"side tracking following slide motion",
"rear shot reflecting in mirrors",
"close-up on concentrated eyes"
]
},
{
"cutId": 4,
"pose": "sitting on floor with legs stretched out",
"action": "wiping sweat from forehead with towel",
"camera": [
"close-up on towel wiping sweat",
"side profile with hair clinging to cheek",
"rear shot showing reflection in mirror",
"wide establishing studio shot",
"handheld zoom-in on tired smile"
]
},
{
"cutId": 5,
"pose": "crouching low with one hand on floor",
"action": "finishing dance move with powerful stance",
"camera": [
"front low angle emphasizing strength",
"side profile showing sweat flying off",
"rear shot with doubled mirror reflection",
"medium shot focusing on determined eyes",
"handheld circular shot for energy"
]
}
]
},
{
"sceneId": 4,
"time": "Morning",
"location": "Park with trees and soft mist",
"outfit": "pastel knit sweater with checkered mini skirt and black stockings",
"cuts": [
{
"cutId": 1,
"pose": "walking slowly on misty path",
"action": "touching leaves as she passes",
"camera": [
"wide shot with mist and sunrays",
"rear tracking shot through trees",
"close-up of hand brushing leaves",
"side profile with foggy background",
"drone rising above misty path"
]
},
{
"cutId": 2,
"pose": "standing still among trees",
"action": "stretching arms out to feel the morning air",
"camera": [
"front wide shot with rays filtering",
"over-shoulder capturing mist depth",
"low angle emphasizing trees and sky",
"medium shot with fog swirling around",
"handheld pan upward following arms"
]
},
{
"cutId": 3,
"pose": "sitting on wooden bench",
"action": "exhaling softly and smiling peacefully",
"camera": [
"side profile with mist behind",
"close-up on serene smile",
"rear shot framing bench and fog",
"slow dolly-in through mist",
"wide landscape shot with her small figure"
]
},
{
"cutId": 4,
"pose": "kneeling on grass with hands in dew",
"action": "looking at water drops sparkling",
"camera": [
"macro close-up of dew on grass",
"low angle framing her hands",
"profile shot with blurred trees",
"overhead soft focus on her and ground",
"slow tilt from grass to her eyes"
]
},
{
"cutId": 5,
"pose": "walking across a small wooden bridge",
"action": "pausing to lean on railing and smile",
"camera": [
"wide shot of bridge in mist",
"rear tracking her walk",
"medium shot of leaning pose",
"close-up on gentle smile",
"drone reveal showing forest below"
]
}
]
},
{
"sceneId": 5,
"time": "Afternoon",
"location": "Flower field under bright blue sky",
"outfit": "pastel pink one-piece dress with white lace stockings",
"cuts": [
{
"cutId": 1,
"pose": "standing with arms open wide",
"action": "spinning slowly among flowers",
"camera": [
"wide drone shot circling her spin",
"low angle capturing flowers and sky",
"tracking shot through flowers toward her",
"rear silhouette against sunlit sky",
"close-up on smiling face with flowers blurred"
]
},
{
"cutId": 2,
"pose": "kneeling among flowers",
"action": "gently touching petals with fingers",
"camera": [
"macro close-up of fingers on petals",
"profile shot with blurred blooms",
"overhead capturing flower pattern",
"low angle with flowers framing face",
"slow dolly-in through blossoms"
]
},
{
"cutId": 3,
"pose": "lying on back in field",
"action": "looking up at clouds and laughing",
"camera": [
"overhead birds-eye view",
"close-up on laughing face with flowers",
"side shot across grass level",
"rear shot with blue sky backdrop",
"handheld zoom-in with shaky intimacy"
]
},
{
"cutId": 4,
"pose": "walking barefoot through flowers",
"action": "lifting skirt slightly while smiling",
"camera": [
"low angle on feet in grass",
"wide tracking shot from side",
"rear dolly-in following footsteps",
"medium shot with skirt lift motion",
"close-up on glowing smile"
]
},
{
"cutId": 5,
"pose": "sitting cross-legged in flowers",
"action": "making a small flower crown",
"camera": [
"front close-up on crown weaving",
"side profile with blurred blooms",
"top-down focusing on hands",
"rack focus from flowers to her eyes",
"wide establishing shot of field"
]
}
]
},
{
"sceneId": 6,
"time": "Afternoon",
"location": "Sunny street with pastel shops and flowers",
"outfit": "pastel yellow mini dress with white knee-high socks and sneakers",
"cuts": [
{
"cutId": 1,
"pose": "standing in middle of street",
"action": "spinning playfully with dress flaring",
"camera": [
"wide establishing shot of street",
"low angle focusing on dress spin",
"tracking dolly circling her",
"rear shot capturing spin from behind",
"slow zoom-in on smiling face"
]
},
{
"cutId": 2,
"pose": "walking along pastel shop windows",
"action": "running fingers along glass",
"camera": [
"side tracking shot at hip level",
"close-up on hand brushing glass",
"reflection shot from window",
"rear follow dolly with blurred people",
"wide shot capturing full storefront"
]
},
{
"cutId": 3,
"pose": "leaning against pastel wall",
"action": "making a heart shape with hands",
"camera": [
"tight close-up on hands forming heart",
"fish-eye wide close-up",
"side shot at 45°",
"POV as if receiving heart",
"zoom burst effect from wide to close"
]
},
{
"cutId": 4,
"pose": "sitting on curb",
"action": "tying shoelaces while smiling up",
"camera": [
"low angle close-up on hands and laces",
"rear over-shoulder capturing face",
"front medium shot with pastel background",
"wide shot with street perspective",
"handheld dolly-in to her smile"
]
},
{
"cutId": 5,
"pose": "holding ice cream",
"action": "taking playful bite and laughing",
"camera": [
"close-up on lips with ice cream",
"profile with blurred shops",
"rear shot with dripping ice cream",
"slow-motion front focus on laugh",
"wide shot with passing bicycles"
]
}
]
},
{
"sceneId": 7,
"time": "Afternoon",
"location": "Trendy café with modern pastel interior",
"outfit": "white blouse tucked into pleated skirt with sheer white stockings",
"cuts": [
{
"cutId": 1,
"pose": "sitting at table with chin on hands",
"action": "smiling softly while tilting head",
"camera": [
"front eye-level close-up",
"over-shoulder with coffee cup foreground",
"soft focus on eyes with blurred café",
"panning shot across table",
"low angle from tabletop upward"
]
},
{
"cutId": 2,
"pose": "standing by window",
"action": "writing playfully in notebook",
"camera": [
"profile with sunlight flare",
"close-up of pen moving on page",
"reflection in window glass",
"tracking dolly up from feet",
"medium shot with soft bokeh"
]
},
{
"cutId": 3,
"pose": "sitting sideways on chair",
"action": "taking sip of coffee",
"camera": [
"tight focus on lips with cup",
"medium framed by chair back",
"dutch angle for playful energy",
"slow dolly-in on sip",
"wide establishing café interior"
]
},
{
"cutId": 4,
"pose": "leaning on counter",
"action": "resting chin on hands dreamily",
"camera": [
"profile with pastel counter lights",
"close-up on dreamy eyes",
"rear shot capturing café depth",
"handheld tilt upward from arms",
"medium shot with blurred background"
]
},
{
"cutId": 5,
"pose": "standing at café entrance",
"action": "waving playfully toward camera",
"camera": [
"wide shot framing doorway",
"rear dolly out into street",
"tight close-up on waving hand",
"low angle capturing door sign",
"tracking shot circling wave"
]
}
]
},
{
"sceneId": 8,
"time": "Afternoon",
"location": "Hilltop path with light mist and mountain view",
"outfit": "lavender cardigan with flared pastel skirt and gray tights",
"cuts": [
{
"cutId": 1,
"pose": "walking up misty hill path",
"action": "holding skirt slightly while climbing",
"camera": [
"rear tracking shot on path",
"low angle capturing feet and mist",
"wide shot with valley below",
"side profile with wind blowing hair",
"drone reveal rising above hill"
]
},
{
"cutId": 2,
"pose": "standing at lookout",
"action": "stretching arms wide into mist",
"camera": [
"front wide shot with horizon",
"rear silhouette against misty valley",
"low angle with sky backdrop",
"close-up on hands spreading",
"slow dolly-in from side"
]
},
{
"cutId": 3,
"pose": "sitting on rock",
"action": "tying hair with calm expression",
"camera": [
"medium shot side profile",
"close-up on hands tying hair",
"rear shot framing valley mist",
"handheld zoom to her smile",
"wide establishing shot with mountains"
]
},
{
"cutId": 4,
"pose": "kneeling on grass",
"action": "picking wildflowers gently",
"camera": [
"macro on hand picking flowers",
"low angle with valley in blur",
"profile with mist behind",
"top-down on her hands",
"slow pan across field to her face"
]
},
{
"cutId": 5,
"pose": "walking down hill path",
"action": "looking back over shoulder smiling",
"camera": [
"rear tracking dolly with path",
"close-up on over-shoulder smile",
"wide shot with valley mist",
"side tracking with flowing skirt",
"drone pull-back revealing whole scene"
]
}
]
},
{
"sceneId": 9,
"time": "Night",
"location": "Balcony overlooking city lights",
"outfit": "elegant black mini dress with sheer black stockings and heels",
"cuts": [
{
"cutId": 1,
"pose": "standing at balcony railing",
"action": "gazing out over city lights",
"camera": [
"rear wide shot with city skyline",
"profile silhouette against glowing lights",
"close-up on hands gripping railing",
"low angle capturing her and the skyline",
"slow dolly-in from doorway"
]
},
{
"cutId": 2,
"pose": "leaning on railing with chin resting",
"action": "smiling softly at the horizon",
"camera": [
"side profile with blurred city bokeh",
"tight close-up on soft smile",
"rear tracking shot along railing",
"medium shot with glowing skyline",
"handheld tilt capturing candid mood"
]
},
{
"cutId": 3,
"pose": "sitting on balcony chair",
"action": "crossing legs elegantly while holding a glass",
"camera": [
"front eye-level medium shot",
"low angle focusing on legs and glass",
"profile silhouette against city",
"close-up on glass near lips",
"wide establishing balcony view"
]
},
{
"cutId": 4,
"pose": "standing near glass door",
"action": "touching window glass gently",
"camera": [
"rear shot with reflection in glass",
"side profile with neon reflection",
"close-up of hand on glass surface",
"soft focus tracking along her arm",
"wide shot framing city lights outside"
]
},
{
"cutId": 5,
"pose": "walking back inside",
"action": "glancing over shoulder toward city",
"camera": [
"rear tracking shot into room",
"close-up on over-shoulder look",
"wide establishing shot of balcony",
"low angle capturing dress movement",
"slow dolly-in on her eyes"
]
}
]
},
{
"sceneId": 10,
"time": "Night",
"location": "Garden with lanterns and candlelight",
"outfit": "flowy white long dress with lace stockings",
"cuts": [
{
"cutId": 1,
"pose": "walking along lantern-lit path",
"action": "holding a lantern gently in hand",
"camera": [
"wide shot with glowing lanterns",
"rear tracking with path perspective",
"close-up on lantern light in hand",
"profile with candle bokeh",
"drone shot rising above path"
]
},
{
"cutId": 2,
"pose": "kneeling by candle arrangement",
"action": "lighting one candle softly",
"camera": [
"macro on candle flame igniting",
"low angle capturing her face glow",
"rear shot with many candles blurred",
"side profile with flickering shadows",
"handheld close-up on gentle smile"
]
},
{
"cutId": 3,
"pose": "sitting at small garden table",
"action": "resting chin on hands dreamily",
"camera": [
"front close-up with lantern foreground",
"side profile framed by soft lights",
"wide shot showing whole garden",
"over-shoulder focusing on glowing eyes",
"slow dolly-in to serene smile"
]
},
{
"cutId": 4,
"pose": "standing beneath lantern tree",
"action": "raising arms as if embracing lights",
"camera": [
"low angle capturing lantern canopy",
"rear silhouette with glowing tree",
"profile with bokeh lights behind",
"wide establishing garden lights",
"tracking circle shot around her pose"
]
},
{
"cutId": 5,
"pose": "walking barefoot on grass",
"action": "spinning lightly with dress flowing",
"camera": [
"rear dolly capturing flowing fabric",
"low angle focusing on feet in grass",
"wide shot with lanterns in background",
"close-up on hair moving in spin",
"handheld pan following her spin"
]
}
]
},
{
"sceneId": 11,
"time": "Night",
"location": "Grand medieval-style ballroom lit by hundreds of candles with golden chandeliers and gothic arches",
"outfit": "elegant layered gown with multiple flowing fabric layers in soft pastel tones, paired with lace stockings and delicate shoes",
"appearance": "hair styled in loose curls with a jeweled tiara, candlelight reflecting on the layered dress",
"cuts": [
{
"cutId": 1,
"pose": "standing gracefully in center of hall with arms slightly lifted",
"action": "beginning a slow dance step, gazing downward softly",
"camera": [
"wide establishing shot of candlelit hall",
"low angle emphasizing layered gown",
"side profile with candles blurred in background",
"rear shot capturing the dress flowing behind",
"close-up on soft serene expression"
]
},
{
"cutId": 2,
"pose": "spinning lightly with gown flaring out",
"action": "smiling gracefully as layers swirl",
"camera": [
"front wide shot capturing gown layers in motion",
"low angle showing candles and fabric layers",
"rear shot with chandeliers glowing above",
"close-up on swirling gown fabric",
"handheld circular dolly around spin"
]
},
{
"cutId": 3,
"pose": "holding skirt edges delicately with both hands",
"action": "stepping forward slowly with elegance",
"camera": [
"medium shot focusing on skirt layers",
"profile capturing her careful step",
"low angle with candlelight flicker",
"rear shot with her shadow on marble floor",
"close-up on hands lifting fabric layers"
]
},
{
"cutId": 4,
"pose": "pausing mid-dance with one hand extended outward",
"action": "gazing toward candlelit balcony dreamily",
"camera": [
"front shot framing extended hand",
"side profile with glowing candles",
"rear wide shot capturing full hall depth",
"close-up on face with candlelight reflections",
"tracking dolly moving slowly toward her"
]
},
{
"cutId": 5,
"pose": "ending dance with a graceful curtsy",
"action": "bowing slightly with layered gown cascading",
"camera": [
"wide shot showing curtsy against hall arches",
"low angle emphasizing fabric folds",
"close-up on gown layers folding gracefully",
"rear shot with chandeliers above her",
"handheld tilt from gown to her smile"
]
}
]
},
{
"sceneId": 12,
"time": "Night",
"location": "Asian summer festival with hundreds of glowing lanterns floating into the night sky",
"outfit": "cute pink summer dress with light fabric and white sandals",
"appearance": "hair gently tied back with loose strands, warm glow of lantern light reflecting on her skin",
"cuts": [
{
"cutId": 1,
"pose": "standing among festival crowd holding a lantern",
"action": "smiling softly as she prepares to release it",
"camera": [
"front medium shot with lantern glowing in hands",
"close-up on her smile illuminated by warm light",
"side profile with lantern crowd blurred",
"rear shot with lanterns flying above",
"wide establishing shot of festival atmosphere"
]
},
{
"cutId": 2,
"pose": "lifting lantern upward with both hands",
"action": "watching it float away with hopeful expression",
"camera": [
"low angle capturing lantern rising above her",
"front wide shot with multiple lanterns",
"close-up on her face lit by lantern glow",
"rear silhouette shot against lantern sky",
"handheld tilt following lantern upward"
]
},
{
"cutId": 3,
"pose": "walking slowly through festival path",
"action": "gazing up at lantern-filled sky",
"camera": [
"wide shot with festival stalls glowing",
"rear tracking shot with lanterns overhead",
"profile with lantern light on her cheek",
"close-up on eyes reflecting lanterns",
"medium shot with soft bokeh background"
]
},
{
"cutId": 4,
"pose": "kneeling to help a child with lantern",
"action": "smiling warmly while adjusting the lantern",
"camera": [
"front medium shot of interaction",
"close-up on hands fixing lantern",
"side profile with glowing lanterns above",
"rear shot with childs lantern rising",
"wide establishing shot with crowd and lights"
]
},
{
"cutId": 5,
"pose": "standing still gazing upward",
"action": "holding hands together in small prayer as lanterns rise",
"camera": [
"low angle with sky full of lanterns",
"front shot focusing on gentle prayer",
"rear silhouette against glowing night sky",
"side profile capturing emotional look",
"drone pull-out showing her among crowd"
]
}
]
}
]
}

Binary file not shown.

View File

@ -0,0 +1,272 @@
{
"character": {
"bodyType": "slim and youthful",
"hairStyle": "long wavy hair with pastel blue and purple colors"
},
"scenes": [
{
"sceneId": 1,
"time": "Morning",
"location": "Cozy bedroom with pastel bedding and sunlight through curtains",
"outfit": "white oversized pajama shirt with thigh-high stockings",
"cuts": [
{
"cutId": 1,
"pose": "lying on bed with head resting on hands",
"action": "smiling softly at the camera",
"camera": [
"overhead shot from above capturing sunlight on her hair",
"close-up of her face with shallow depth of field",
"side angle showing body stretched on bed",
"slow zoom-in from doorway",
"handheld camera wobble for intimate feeling"
]
},
{
"cutId": 2,
"pose": "stretching arms above head while sitting on bed",
"action": "yawning cutely with eyes half closed",
"camera": [
"medium shot from foot of bed",
"low angle from floor emphasizing legs",
"wide shot with window light flaring",
"tracking shot circling around her stretch",
"soft focus tilt-shift framing"
]
},
{
"cutId": 3,
"pose": "kneeling on bed with playful look",
"action": "blowing a kiss to the camera",
"camera": [
"front close-up catching kiss in slow motion",
"wide shot with pastel background",
"camera tilt from below lips to eyes",
"360° pan around her kneeling pose",
"handheld push-in as she blows kiss"
]
}
]
},
{
"sceneId": 2,
"time": "Morning",
"location": "Bedroom near window with plants and sunlight",
"outfit": "light pastel camisole with short pleated skirt and stockings",
"cuts": [
{
"cutId": 1,
"pose": "sitting on window sill, legs slightly crossed",
"action": "looking outside then turning to smile",
"camera": [
"silhouette against sunlight",
"side profile with plants blurred in foreground",
"close-up of smile turning to camera",
"dolly-in from outside window glass",
"over-shoulder shot capturing view outside"
]
},
{
"cutId": 2,
"pose": "leaning against the wall, hands behind back",
"action": "giggling with shy expression",
"camera": [
"eye-level shot with wall texture visible",
"slight high angle to emphasize cuteness",
"medium close-up on giggle with tilt",
"rack focus between wall décor and her face",
"soft handheld sway left to right"
]
},
{
"cutId": 3,
"pose": "lying on floor with legs up against the wall",
"action": "kicking feet playfully while laughing",
"camera": [
"top-down view from ceiling",
"low angle from foot level",
"side shot capturing playful kicks",
"slow pan across her body",
"handheld camera zoom-in to laughter"
]
}
]
},
{
"sceneId": 3,
"time": "Afternoon",
"location": "Sunny street with pastel shops and flowers",
"outfit": "short pastel pink dress with white stockings and sneakers",
"cuts": [
{
"cutId": 1,
"pose": "standing with one hand on hip",
"action": "spinning gently in place",
"camera": [
"full body wide shot with pastel shops",
"low angle capturing dress swirl",
"tracking shot circling spin",
"handheld slow zoom-in to face",
"rear shot revealing spin from behind"
]
},
{
"cutId": 2,
"pose": "walking with small steps",
"action": "waving happily at the camera",
"camera": [
"tracking dolly shot in front",
"overhead drone shot of street",
"side follow shot at hip level",
"handheld jitter to mimic vlog",
"close-up on waving hand with face blurred"
]
},
{
"cutId": 3,
"pose": "leaning forward playfully",
"action": "making a heart shape with hands",
"camera": [
"tight close-up on hands forming heart",
"fish-eye wide close-up",
"side shot at 45°",
"POV shot as if receiving heart",
"zoom burst effect from wide to close"
]
}
]
},
{
"sceneId": 4,
"time": "Afternoon",
"location": "Trendy café with bright modern interior",
"outfit": "white blouse tucked into pleated skirt with stockings",
"cuts": [
{
"cutId": 1,
"pose": "sitting at a table with chin on hands",
"action": "smiling softly while tilting head",
"camera": [
"front eye-level close-up",
"soft focus on eyes, blurred background",
"over-shoulder shot of coffee cup",
"panning shot across table to her",
"low angle from table surface"
]
},
{
"cutId": 2,
"pose": "standing by window with crossed legs",
"action": "writing playfully in a small notebook",
"camera": [
"profile with sunlight flare",
"close-up of pen on paper then tilt to face",
"tracking shot from feet to head",
"reflection in window glass",
"medium shot with bokeh lights behind"
]
},
{
"cutId": 3,
"pose": "sitting sideways on chair with one leg up",
"action": "taking a sip from a cup with cute expression",
"camera": [
"tight focus on lips touching cup",
"medium shot framed by chair back",
"slight dutch angle for energy",
"slow dolly-in on playful sip",
"wide establishing shot of café"
]
}
]
},
{
"sceneId": 5,
"time": "Night",
"location": "Elegant ballroom with chandeliers",
"outfit": "sparkly silver mini dress with black stockings and heels",
"cuts": [
{
"cutId": 1,
"pose": "standing tall with one hand on waist",
"action": "turning slowly while smiling confidently",
"camera": [
"wide shot capturing chandelier",
"low angle emphasizing elegance",
"tracking dolly rotation",
"close-up of smile during turn",
"rear tracking shot revealing gown shimmer"
]
},
{
"cutId": 2,
"pose": "sitting gracefully on a velvet chair",
"action": "crossing legs elegantly",
"camera": [
"medium close-up from side",
"top-down angle showing chair texture",
"front focus on crossed legs",
"soft rack focus from chair to her face",
"panning shot circling chair"
]
},
{
"cutId": 3,
"pose": "leaning on railing with dreamy look",
"action": "gazing at the chandelier lights",
"camera": [
"over-shoulder shot of chandelier view",
"profile silhouette with golden backlight",
"wide shot capturing ballroom depth",
"handheld tilt up from railing to face",
"slow dolly-out revealing emptiness of hall"
]
}
]
},
{
"sceneId": 6,
"time": "Night",
"location": "Luxurious bedroom with soft golden lighting",
"outfit": "black lace camisole with mini skirt and stockings",
"cuts": [
{
"cutId": 1,
"pose": "lying sideways on bed with legs slightly bent",
"action": "looking at the camera with sultry eyes",
"camera": [
"close-up on eyes with blurred background",
"tracking shot along legs up to face",
"overhead soft focus",
"low angle from bed surface",
"handheld intimate pan across body"
]
},
{
"cutId": 2,
"pose": "sitting at vanity table",
"action": "putting on earrings slowly",
"camera": [
"mirror reflection focus",
"close-up on hands adjusting earrings",
"profile side shot with warm glow",
"over-shoulder shot including vanity lights",
"slow push-in from doorway"
]
},
{
"cutId": 3,
"pose": "kneeling on bed with arched back",
"action": "running hand through hair with sensual expression",
"camera": [
"rear shot with back arch emphasized",
"medium close-up focusing on hair movement",
"low angle capturing curves",
"soft focus candlelight bokeh",
"circling dolly shot for dramatic effect"
]
}
]
}
]
}

View File

@ -0,0 +1,259 @@
import dotenv from 'dotenv';
import path from 'path';
import fs from 'fs/promises';
import { logger } from '../lib/logger';
import { callOpenAI } from '../lib/openai';
import { generateVideo } from '../lib/video-generator';
dotenv.config();
type Size = { width: number; height: number };
interface MusicSpotCharacter {
bodyType: string;
hairStyle: string;
}
interface MusicSpotCut {
cutId: number;
pose: string;
action: string;
camera?: string[]; // list of camera variants per cut
}
interface MusicSpotScene {
sceneId: number;
time: string;
location: string;
outfit: string;
cuts: MusicSpotCut[];
}
interface MusicSpotConfig {
character: MusicSpotCharacter;
scenes: MusicSpotScene[];
}
interface Server {
baseUrl?: string;
outputDir?: string;
inputDir?: string;
name: string;
}
const DEFAULT_SIZE: Size = { width: 720, height: 1280 };
const FOLDER = process.argv[2] || process.env.MUSICSPOT_FOLDER || 'oputstise';
const FOLDER_SAFE = FOLDER.replace(/[/\\?%*:|"<>]/g, '_');
const GENERATED_DIR = path.resolve('generated');
function loadServers(): Server[] {
const servers: Server[] = [
{
name: 'SERVER1',
baseUrl: process.env.SERVER1_COMFY_BASE_URL,
outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR,
},
/*
{
name: 'SERVER2',
baseUrl: process.env.SERVER2_COMFY_BASE_URL,
outputDir: process.env.SERVER2_COMFY_OUTPUT_DIR,
},*/
]
.filter((s) => !!s.baseUrl && !!s.outputDir)
.map((s) => ({
...s,
inputDir: s.outputDir!.replace(/output/i, 'input'),
}));
if (servers.length === 0) {
logger.warn('No servers configured. Please set SERVER{N}_COMFY_BASE_URL and SERVER{N}_COMFY_OUTPUT_DIR in .env');
} else {
for (const s of servers) {
logger.info(`Configured ${s.name}: baseUrl=${s.baseUrl}, outputDir=${s.outputDir}, inputDir=${s.inputDir}`);
}
}
return servers;
}
async function ensureDirs() {
await fs.mkdir(GENERATED_DIR, { recursive: true });
}
function buildVideoPromptRequest(
character: MusicSpotCharacter,
scene: MusicSpotScene,
cut: MusicSpotCut,
cameraIntent: string
): string {
return `
Return exactly one JSON object and nothing else: { "videoPrompt": "..." }.
Write "videoPrompt" in 100140 words. Present tense. Concrete, simple sentences.
HARD RULES:
- One continuous 8-second shot (oner). No edits.
- Fixed location and general vantage; maintain spatial continuity.
- No zooms, no rack zoom, no smash/push-in, no cuts, no transitions, no "meanwhile".
- Camera motion: at most a slight pan/tilt or subtle dolly within 1 meter.
- Keep framing consistent (vertical 720x1280). Avoid technical brand names or lens jargon.
Incorporate the following camera intention: "${cameraIntent}".
If it conflicts with HARD RULES (e.g., zoom, push-in, extreme moves), reinterpret it into a subtle, compliant motion (e.g., gentle glide, slight pan/tilt) while preserving the creative intent.
Describe:
1) Main action: ${cut.action}
2) Pose/composition: ${cut.pose}
3) Scene/time/location/outfit: ${scene.time}; ${scene.location}; outfit: ${scene.outfit}
4) Lighting/mood/style coherent with the character: ${character.bodyType}; hair: ${character.hairStyle}
Prohibited (case-insensitive): cut, cuts, cutting, quick cut, insert, close-up, extreme close-up, zoom, zooming, push-in, pull-out, whip, switch angle, change angle, montage, cross-cut, smash cut, transition, meanwhile, later.
Only respond with JSON.
`.trim();
}
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
async function getVideoPromptFromOpenAI(req: string): Promise<string> {
const res = await callOpenAI(req);
const prompt = res?.videoPrompt || res?.video_prompt || res?.prompt;
if (!prompt || typeof prompt !== 'string') {
throw new Error('OpenAI failed to return videoPrompt JSON.');
}
return prompt.trim();
}
function pickServer(servers: Server[], idx: number): Server {
if (servers.length === 0) {
throw new Error('No servers configured.');
}
return servers[idx % servers.length];
}
async function copyImageToAllServerInputs(servers: Server[], localGeneratedImagePath: string): Promise<string> {
const fileName = path.basename(localGeneratedImagePath);
for (const s of servers) {
if (!s.inputDir) continue;
const dest = path.join(s.inputDir, fileName);
try {
await fs.copyFile(localGeneratedImagePath, dest);
logger.debug(`Copied ${fileName} to ${s.name} input: ${dest}`);
} catch (err) {
logger.warn(`Failed to copy ${fileName} to ${s.name} input: ${err}`);
}
}
return fileName; // return the name used for Comfy workflows
}
async function fileExists(p: string): Promise<boolean> {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
async function main() {
try {
await ensureDirs();
// Load scenes.json
const configRaw = await fs.readFile(path.resolve(`src/musicspot_generator/${FOLDER}/scenes.json`), 'utf-8');
const cfg: MusicSpotConfig = JSON.parse(configRaw);
const servers = loadServers();
if (servers.length === 0) {
return;
}
// Generate videos only, based on images already present in ./generated
let videoTaskIndex = 0;
for (const scene of cfg.scenes) {
logger.info(`=== Scene ${scene.sceneId}: Video generation start ===`);
for (const cut of scene.cuts) {
const cameraVariants =
Array.isArray(cut.camera) && cut.camera.length > 0
? cut.camera
: ['eye-level medium shot', 'slight left 30°', 'slight right 30°', 'slight high angle', 'slight low angle'];
for (let camIdx = 0; camIdx < cameraVariants.length; camIdx++) {
const cameraIntent = cameraVariants[camIdx];
const variantIndex = camIdx + 1;
const imgFileName = `${FOLDER_SAFE}_musicspot_s${scene.sceneId}_c${cut.cutId}_v${variantIndex}.png`;
const imgPath = path.join(GENERATED_DIR, imgFileName);
// Only proceed if image exists
const hasImage = await fileExists(imgPath);
if (!hasImage) {
logger.warn(`Skipping video: source image not found: ${imgPath}`);
continue;
}
const videoFileName = imgFileName.replace(/\.png$/i, '.mp4');
const videoOutPath = path.join(GENERATED_DIR, videoFileName);
// Skip if video already
const hasVideo = await fileExists(videoOutPath);
if (hasVideo) {
logger.info(`Video already exists, skipping: ${videoOutPath}`);
continue;
}
// 1) Generate video prompt for this camera
logger.info(
`Scene ${scene.sceneId} - Cut ${cut.cutId} - Cam${variantIndex}: generating video prompt from image ${imgFileName}...`
);
const vidPromptReq = buildVideoPromptRequest(cfg.character, scene, cut, cameraIntent);
let videoPrompt: string;
try {
videoPrompt = await getVideoPromptFromOpenAI(vidPromptReq);
} catch (err) {
logger.error(`OpenAI video prompt failed for ${imgFileName}: ${err}`);
continue;
}
// 2) Copy the base image to every server's input folder
const imageFileNameForComfy = await copyImageToAllServerInputs(servers, imgPath);
// 3) Generate video on a chosen server (round-robin)
const serverForVideo = pickServer(servers, videoTaskIndex++);
logger.info(`Generating video (${videoFileName}) on ${serverForVideo.name} using ${imageFileNameForComfy}...`);
try {
const videoPath = await generateVideo(
videoPrompt,
imageFileNameForComfy,
videoFileName,
serverForVideo.baseUrl!,
serverForVideo.outputDir!,
DEFAULT_SIZE,
true,
true
);
await sleep(10000); // wait a bit for file system to settle
logger.info(`Video generated: ${videoPath}`);
} catch (err) {
logger.error(`Video generation failed (${videoFileName}) on ${serverForVideo.name}: ${err}`);
}
}
}
logger.info(`=== Scene ${scene.sceneId}: Video generation complete ===`);
}
logger.info('Video generation for all scenes completed.');
} catch (err) {
logger.error('Fatal error in music spot video generator:', err);
}
}
main().catch((err) => {
logger.error('Unhandled error:', err);
});