Merge branch 'master' of https://git.yasue.org/ken/RandomVideoMaker
195
src/comfyworkflows/generate_image_with_face.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -216,7 +216,7 @@
|
||||
},
|
||||
"63": {
|
||||
"inputs": {
|
||||
"frame_rate": 32,
|
||||
"frame_rate": 25,
|
||||
"loop_count": 0,
|
||||
"filename_prefix": "RADOMVIDEOMAKERVIDEO",
|
||||
"format": "video/h264-mp4",
|
||||
|
||||
349
src/comfyworkflows/generate_video_light.json
Normal 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 boy’s 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": 25,
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
src/infinityvideo_generator/initial_image.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
4
src/infinityvideo_generator/scene.json
Normal 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."
|
||||
}
|
||||
395
src/infinityvideo_generator/start.ts
Normal 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);
|
||||
});
|
||||
77
src/lib/image-generator-face.ts
Normal 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 };
|
||||
@ -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);
|
||||
|
||||
BIN
src/musicspot_generator/cetridesete/face.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
src/musicspot_generator/cetridesete/face2.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
598
src/musicspot_generator/cetridesete/scenes.json
Normal file
@ -0,0 +1,598 @@
|
||||
{
|
||||
"song": {
|
||||
"title": "Četrdesete: Svjetlo kroz Maglu",
|
||||
"artist": "Radni Front",
|
||||
"genre": "Cinematic Pop/Rock",
|
||||
"mood": "Gritty, uplifting, hopeful"
|
||||
},
|
||||
"character": {
|
||||
"bodyType": "average build, slightly athletic, in his 40s",
|
||||
"hairStyle": "short side-part, light stubble",
|
||||
"accessories": "leather messenger bag, analog watch, simple ring"
|
||||
},
|
||||
"scenes": [
|
||||
{
|
||||
"sceneId": 1,
|
||||
"time": "Morning",
|
||||
"location": "Foggy city street at dawn",
|
||||
"outfit": "charcoal wool coat over navy blazer, white oxford shirt, dark chinos, leather derbies, grey scarf; takeaway coffee in hand",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "walking briskly with coffee",
|
||||
"action": "exhaling visible breath in cold air",
|
||||
"camera": [
|
||||
"full-body frontal walk through light shafts in fog",
|
||||
"low angle on shoes splashing a thin puddle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "pausing at crosswalk",
|
||||
"action": "checking watch then stepping forward",
|
||||
"camera": [
|
||||
"close-up on watch face catching sun flare",
|
||||
"three-quarter side track across zebra lines"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "tight grip on cup",
|
||||
"action": "lifting cup for a sip",
|
||||
"camera": [
|
||||
"macro fingers on cup sleeve with steam",
|
||||
"profile head-and-shoulders backlit by hazy sun"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "steady stride",
|
||||
"action": "adjusting scarf in breeze",
|
||||
"camera": [
|
||||
"telephoto compression down foggy avenue",
|
||||
"rear follow shot with scarf rim-lit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "determined gaze",
|
||||
"action": "looking ahead into sunlight",
|
||||
"camera": [
|
||||
"tight face with soft lens bloom",
|
||||
"over-shoulder toward blinding sky gap"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 2,
|
||||
"time": "Morning",
|
||||
"location": "Tram interior rocking through city",
|
||||
"outfit": "navy blazer, knit crewneck, white shirt, dark jeans, leather sneakers; coffee in hand",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "standing in tram",
|
||||
"action": "holding strap with one hand, coffee in the other",
|
||||
"camera": [
|
||||
"medium inside tram, window condensation streaks",
|
||||
"front angle from aisle with passing light bands"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "leaning against pole",
|
||||
"action": "checking phone briefly",
|
||||
"camera": [
|
||||
"over-shoulder to phone screen glow",
|
||||
"side profile with rim light from window"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "looking out window",
|
||||
"action": "soft smile seeing morning city",
|
||||
"camera": [
|
||||
"through-glass face framed by droplets",
|
||||
"exterior-to-interior angle catching reflection layers"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "steady stance",
|
||||
"action": "taking small sip as tram turns",
|
||||
"camera": [
|
||||
"low angle on cup tilt and sleeve",
|
||||
"rear quarter shot showing sway of carriage"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "composed",
|
||||
"action": "pocketing phone, focusing forward",
|
||||
"camera": [
|
||||
"chest-up push-in down the aisle",
|
||||
"top-down seat pattern with soft bloom"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 3,
|
||||
"time": "Morning",
|
||||
"location": "Modern office desk with soft skylight",
|
||||
"outfit": "light grey blazer, pale blue shirt, dark chinos, brown brogues; ID lanyard",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "seated typing",
|
||||
"action": "focused work with subtle nods",
|
||||
"camera": [
|
||||
"medium desk-side angle with window beams",
|
||||
"close-up fingers on keyboard with keycap sheen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "reviewing papers",
|
||||
"action": "marking with pen",
|
||||
"camera": [
|
||||
"top-down documents in stripey sunlight",
|
||||
"front low angle across desk edge"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "phone to ear",
|
||||
"action": "short professional conversation",
|
||||
"camera": [
|
||||
"profile with rim-lit jawline",
|
||||
"over-shoulder to city glow outside"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "stretching shoulders",
|
||||
"action": "exhaling, sip of water",
|
||||
"camera": [
|
||||
"medium with glass prisming light",
|
||||
"close-up on water surface ripples"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "refocused",
|
||||
"action": "resuming typing with resolve",
|
||||
"camera": [
|
||||
"front push-in past monitor edge",
|
||||
"rear silhouette against bright window"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 4,
|
||||
"time": "Late Morning",
|
||||
"location": "Office glass meeting room",
|
||||
"outfit": "navy sport coat, white shirt, knit tie, tailored trousers",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "standing at screen",
|
||||
"action": "pointing to chart, calm tone",
|
||||
"camera": [
|
||||
"three-quarter presenter through glass reflections",
|
||||
"over-shoulder to slide glow on faces"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "listening",
|
||||
"action": "nodding, hands clasped",
|
||||
"camera": [
|
||||
"medium frontal with rim edge light",
|
||||
"tight hands interlaced catching sheen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "whiteboard note",
|
||||
"action": "writing key word",
|
||||
"camera": [
|
||||
"side on marker tip against board",
|
||||
"over-shoulder on word under skylight stripe"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "hand gestures",
|
||||
"action": "framing idea in air",
|
||||
"camera": [
|
||||
"front medium with floating dust motes",
|
||||
"profile gesture silhouette against window"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "closing remark",
|
||||
"action": "small confident smile",
|
||||
"camera": [
|
||||
"tight smile with gentle bloom",
|
||||
"wide room applauding in soft backlight"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 5,
|
||||
"time": "Midday",
|
||||
"location": "Quiet park bench under soft sun",
|
||||
"outfit": "olive field jacket over white shirt, navy chinos, sneakers",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "seated on bench",
|
||||
"action": "unwrapping sandwich",
|
||||
"camera": [
|
||||
"wide bench under dappled leaves",
|
||||
"close-up foil crackle catching sparkles"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "bite and look up",
|
||||
"action": "watching birds",
|
||||
"camera": [
|
||||
"tight mouthful with flare peeking",
|
||||
"low up-angle to glowing canopy"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "relaxed lean",
|
||||
"action": "closing eyes briefly",
|
||||
"camera": [
|
||||
"front push-in eyes closed in sun warmth",
|
||||
"overhead bench and soft shadow lace"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "checking phone",
|
||||
"action": "soft smile at message",
|
||||
"camera": [
|
||||
"over-shoulder to screen in shade",
|
||||
"side profile with sun peeking lens flare"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "standing stretch",
|
||||
"action": "deep breath hands on hips",
|
||||
"camera": [
|
||||
"rear sun halo and long shadow",
|
||||
"front medium with leaf bokeh confetti"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 6,
|
||||
"time": "Afternoon",
|
||||
"location": "Open office collaboration area",
|
||||
"outfit": "rolled-sleeve shirt, blazer off, vest optional, dark trousers",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "standing with teammates",
|
||||
"action": "high-five and laugh",
|
||||
"camera": [
|
||||
"wide group under skylight glow",
|
||||
"tight palms meeting with light burst"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "pointing at laptop",
|
||||
"action": "explaining step-by-step",
|
||||
"camera": [
|
||||
"over-shoulder to UI reflecting in eyes",
|
||||
"front trio with bright window beams"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "shared joke",
|
||||
"action": "covering mouth laughing",
|
||||
"camera": [
|
||||
"tight eyes creasing with sparkle",
|
||||
"side candid smiles with lens bloom"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "sticky note placement",
|
||||
"action": "slapping idea on board",
|
||||
"camera": [
|
||||
"macro note corner catching shine",
|
||||
"front push-in to colorful board grid"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "wrap-up clap",
|
||||
"action": "small bow to team",
|
||||
"camera": [
|
||||
"medium applause with soft halos",
|
||||
"rear shoulders rim-lit against corridor"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 7,
|
||||
"time": "Evening",
|
||||
"location": "Recording studio vocal booth",
|
||||
"outfit": "black henley under dark blazer, dark jeans; studio headphones, pop filter",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "standing at mic",
|
||||
"action": "closing eyes and singing",
|
||||
"camera": [
|
||||
"medium mic and pop filter with tube glow",
|
||||
"profile head tilt with headphone sheen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "one hand on headphone",
|
||||
"action": "finding pitch",
|
||||
"camera": [
|
||||
"tight hand on ear cup",
|
||||
"over-shoulder to waveform screen in control room"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "leaning toward mic",
|
||||
"action": "delivering powerful line",
|
||||
"camera": [
|
||||
"front intense eyes past grille",
|
||||
"low angle along mic stand to face"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "step back",
|
||||
"action": "nod to beat, smile",
|
||||
"camera": [
|
||||
"wide booth with warm bokeh bulbs",
|
||||
"tight smile reflected in glass"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "hands open",
|
||||
"action": "finishing phrase softly",
|
||||
"camera": [
|
||||
"front soft flare across frame",
|
||||
"overhead hands lowering into light cone"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 8,
|
||||
"time": "Golden Hour",
|
||||
"location": "City park path with family",
|
||||
"outfit": "sand blazer, light knit, dark denim; family in cozy casual",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "holding partner’s hand",
|
||||
"action": "walking with child skipping ahead",
|
||||
"camera": [
|
||||
"rear wide with low sun flare",
|
||||
"front medium through tree bokeh"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "lifting child",
|
||||
"action": "spinning gently",
|
||||
"camera": [
|
||||
"front slow shutter light trails around",
|
||||
"top-down arms forming circle with sun rim"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "group hug",
|
||||
"action": "closing eyes contentedly",
|
||||
"camera": [
|
||||
"tight embrace hands with ring glint",
|
||||
"side cheek-to-hair halo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "pointing to sky",
|
||||
"action": "showing plane to child",
|
||||
"camera": [
|
||||
"up-angle to contrail through warm haze",
|
||||
"front medium faces reflecting sky"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "hands intertwined",
|
||||
"action": "walking toward camera",
|
||||
"camera": [
|
||||
"front slow push with golden confetti bokeh",
|
||||
"low angle on joined hands swinging"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 9,
|
||||
"time": "Night",
|
||||
"location": "City viewpoint with beautiful skyline",
|
||||
"outfit": "charcoal overcoat, black turtleneck, tailored trousers",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "leaning on railing",
|
||||
"action": "taking in skyline",
|
||||
"camera": [
|
||||
"wide skyline as glittering backdrop",
|
||||
"profile rim light from city glow"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "deep breath",
|
||||
"action": "exhale mist into night",
|
||||
"camera": [
|
||||
"tight breath vapor lit by neon",
|
||||
"rear silhouette against bokeh lights"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "phone photo",
|
||||
"action": "capturing skyline",
|
||||
"camera": [
|
||||
"over-shoulder to bright screen framing towers",
|
||||
"low angle to phone and light crown above"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "hands in pockets",
|
||||
"action": "swaying to distant music",
|
||||
"camera": [
|
||||
"medium chest-up with drifting car streaks",
|
||||
"tight shoe tapping in puddle reflection"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "confident gaze",
|
||||
"action": "looking past camera",
|
||||
"camera": [
|
||||
"front tight eyes with city sparkles",
|
||||
"top-down platform grid glimmering"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 10,
|
||||
"time": "Night",
|
||||
"location": "Office district streets after hours",
|
||||
"outfit": "dark blazer, open collar shirt, slim trousers, chelsea boots",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "walking alone",
|
||||
"action": "hands in pockets",
|
||||
"camera": [
|
||||
"long-lens frontal with pearl-like streetlights",
|
||||
"rear follow on wet pavement reflections"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "pause under lamp",
|
||||
"action": "looking up between towers",
|
||||
"camera": [
|
||||
"upward angle of converging buildings and lamp star",
|
||||
"side silhouette against lobby glow"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "check messages",
|
||||
"action": "small nod of relief",
|
||||
"camera": [
|
||||
"over-shoulder phone light on face",
|
||||
"handheld parallax of glass light columns"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "crossing street",
|
||||
"action": "steady stride",
|
||||
"camera": [
|
||||
"wide zebra stripes with mirror reflections",
|
||||
"low angle heel strike and passing headlight streak"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "brief smile",
|
||||
"action": "exhale, continue walking",
|
||||
"camera": [
|
||||
"tight smile with neon glimmer",
|
||||
"front slow push through flare streaks"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 11,
|
||||
"time": "Night",
|
||||
"location": "Home living room, warm lamps",
|
||||
"outfit": "soft cardigan over tee, lounge chinos, socks",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "sinking into sofa",
|
||||
"action": "deep relieved breath",
|
||||
"camera": [
|
||||
"medium cozy frame with lamp bloom",
|
||||
"low angle to floor lamp starburst"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "family photo glance",
|
||||
"action": "gentle smile at frame",
|
||||
"camera": [
|
||||
"close-up photo glass catching sparkle",
|
||||
"side hand brushing frame with glint"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "stretch and yawn",
|
||||
"action": "neck roll, relaxed shoulders",
|
||||
"camera": [
|
||||
"front medium as light wraps softly",
|
||||
"rear silhouette against curtained window halo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "notebook moment",
|
||||
"action": "jotting tomorrow’s plan",
|
||||
"camera": [
|
||||
"top-down page and pen tip glint",
|
||||
"three-quarter profile calm focus"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "closing lights",
|
||||
"action": "switching lamp off",
|
||||
"camera": [
|
||||
"tight finger on switch—tiny spark",
|
||||
"wide room dimming to city twinkle outside"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
68
src/musicspot_generator/cetridesete/scenes2.json
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"song": {
|
||||
"title": "Četrdesete: Svjetlo kroz Maglu",
|
||||
"artist": "Radni Front",
|
||||
"genre": "Cinematic Pop/Rock",
|
||||
"mood": "Gritty, uplifting, hopeful"
|
||||
},
|
||||
"character": {
|
||||
"bodyType": "average build, slightly athletic, in his 40s",
|
||||
"hairStyle": "short side-part, light stubble",
|
||||
"accessories": "leather messenger bag, analog watch, simple ring"
|
||||
},
|
||||
"scenes": [
|
||||
{
|
||||
"sceneId": 1,
|
||||
"time": "Morning",
|
||||
"location": "Foggy city street at dawn",
|
||||
"outfit": "down jacket, scarf, jeans, boots, t-shirt, hiphop style",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "walking ",
|
||||
"action": "exhaling visible breath in cold air",
|
||||
"camera": [
|
||||
"full-body frontal walk through light shafts in fog",
|
||||
"low angle on shoes splashing a thin puddle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "walking hands in pockets",
|
||||
"action": "",
|
||||
"camera": [
|
||||
"close-up on watch face catching sun flare",
|
||||
"three-quarter side track across zebra lines"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "standing hands in pockets",
|
||||
"action": "",
|
||||
"camera": [
|
||||
"macro fingers on cup sleeve with steam",
|
||||
"profile head-and-shoulders backlit by hazy sun"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "steady stride",
|
||||
"action": "adjusting scarf in breeze",
|
||||
"camera": [
|
||||
"telephoto compression down foggy avenue",
|
||||
"rear follow shot with scarf rim-lit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "determined gaze",
|
||||
"action": "looking ahead into sunlight",
|
||||
"camera": [
|
||||
"tight face with soft lens bloom",
|
||||
"over-shoulder toward blinding sky gap"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
src/musicspot_generator/cetridesete/song.mp3
Normal file
BIN
src/musicspot_generator/fire in the night/face.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
786
src/musicspot_generator/fire in the night/scenes.json
Normal file
@ -0,0 +1,786 @@
|
||||
{
|
||||
"song": {
|
||||
"title": "Svjetlo kroz Maglu",
|
||||
"artist": "Radni Dan",
|
||||
"genre": "Pop Rock / Motivational Rap",
|
||||
"mood": "Hopeful, gritty, determined"
|
||||
},
|
||||
"character": {
|
||||
"bodyType": "average 40s male, sturdy build",
|
||||
"hairStyle": "short neat hair with slight side part, light stubble",
|
||||
"outfitPalette": "navy, charcoal, camel, white, subtle textures",
|
||||
"defaultOutfit": "smart-casual: tailored blazer, oxford shirt (top button open), slim chinos, leather sneakers or derby shoes, minimalist watch, messenger bag"
|
||||
},
|
||||
"scenes": [
|
||||
{
|
||||
"sceneId": 1,
|
||||
"lyricAnchor": "intro",
|
||||
"time": "Early Morning",
|
||||
"location": "Foggy street in a quiet city block",
|
||||
"outfit": "camel coat over navy blazer and white oxford, charcoal chinos, leather sneakers; steaming paper coffee cup in hand",
|
||||
"lightFX": "soft ground fog, cool blue ambience, warm shop-window spill, rays through mist",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "walking steadily with coffee, shoulders relaxed",
|
||||
"action": "breath visible in cold air",
|
||||
"camera": [
|
||||
"full body tracking through fog with distant headlights bokeh",
|
||||
"side shot catching coffee steam in backlight",
|
||||
"face zoom in with gentle lens flare from streetlamp",
|
||||
"overhead drone-lite glide showing puddle reflections",
|
||||
"back shot with long god rays slicing mist"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "pauses at crosswalk",
|
||||
"action": "checks watch, sips coffee",
|
||||
"camera": [
|
||||
"low angle shoe splash past a glowing puddle",
|
||||
"medium shot watch face catching specular highlight",
|
||||
"face zoom in through drifting fog",
|
||||
"back shot red-to-green signal glow on mist",
|
||||
"overhead crosswalk stripes fading into haze"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "resumes walk",
|
||||
"action": "adjusts strap of messenger bag",
|
||||
"camera": [
|
||||
"tracking three-quarter with storefront light streaks",
|
||||
"side shot shallow DOF light orbs (bokeh)",
|
||||
"face zoom in calm focus",
|
||||
"reflected view in wet window pane",
|
||||
"wide establishing with soft sunrise rim light"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "stops to let bicycle pass",
|
||||
"action": "nods politely",
|
||||
"camera": [
|
||||
"over-shoulder bicycle headlights streaking",
|
||||
"medium profile with fog swirls lit from back",
|
||||
"ground-level reflection of both in puddle",
|
||||
"wide silhouette against pale dawn sky",
|
||||
"face close-up catching tiny snowlike mist droplets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "steps onto tram platform edge",
|
||||
"action": "exhales, looks down the track",
|
||||
"camera": [
|
||||
"back shot rails vanishing into fog",
|
||||
"side shot coffee steam crossing light beam",
|
||||
"face zoom in hopeful gaze",
|
||||
"overhead platform LEDs forming dotted trail",
|
||||
"low angle rails with subtle lens flare"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 2,
|
||||
"lyricAnchor": "verse A",
|
||||
"time": "Morning",
|
||||
"location": "City tram interior, gently swaying",
|
||||
"outfit": "navy blazer, white oxford, grey chinos; coffee cup; transit card",
|
||||
"lightFX": "cool daylight through foggy windows, window streak glares, rhythmic flicker of tunnels",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "standing near door, one hand on pole",
|
||||
"action": "sips coffee, eyes on passing streets",
|
||||
"camera": [
|
||||
"full body with window streaks bokeh outside",
|
||||
"side shot with parallax of city blur",
|
||||
"face zoom in catching soft rim light",
|
||||
"reflection shot in tram door glass",
|
||||
"overhead passengers’ silhouettes in soft haze"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "leans on pole",
|
||||
"action": "checks phone briefly (calendar alert)",
|
||||
"camera": [
|
||||
"close on phone UI glow reflecting on face",
|
||||
"low angle hand + coffee, light streaks above",
|
||||
"medium shot over-shoulder to window",
|
||||
"face zoom in steadied breathing",
|
||||
"wide carriage shot with light pulses"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "shifts stance with sway",
|
||||
"action": "tightens blazer button",
|
||||
"camera": [
|
||||
"profile tightening button with specular highlight",
|
||||
"back shot rows of seats in perspective",
|
||||
"face zoom in determined eyes",
|
||||
"window reflection double exposure feel",
|
||||
"handheld subtle sway for realism"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "glances at a child smiling",
|
||||
"action": "returns a soft smile",
|
||||
"camera": [
|
||||
"medium two-shot with warm sun beam",
|
||||
"face zoom in softened expression",
|
||||
"side shot catching dust motes in light",
|
||||
"overhead strap lights forming leading line",
|
||||
"wide interior with gentle motion blur"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "tram slows",
|
||||
"action": "prepares to step out",
|
||||
"camera": [
|
||||
"close on shoes as doors open, light washes floor",
|
||||
"back shot doors parting with faint lens flare",
|
||||
"face three-quarter with morning glow",
|
||||
"low angle step-down with steam from cup",
|
||||
"wide exterior as he exits into bright haze"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 3,
|
||||
"lyricAnchor": "verse A",
|
||||
"time": "Late Morning",
|
||||
"location": "Modern office, clean lines, window light",
|
||||
"outfit": "blazer off on chair, rolled sleeves, oxford + chinos; ID badge",
|
||||
"lightFX": "soft skylight, bounce from white desks, screen glow highlights",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "seated typing",
|
||||
"action": "focused, steady pace",
|
||||
"camera": [
|
||||
"over-shoulder code/docs on monitor with gentle bloom",
|
||||
"side shot keypress rhythm, wristwatch glint",
|
||||
"face zoom in concentration with catchlight",
|
||||
"overhead tidy desk grid, coffee ring",
|
||||
"wide office depth with sunbeams"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "stands to stretch",
|
||||
"action": "rolls shoulders, exhales",
|
||||
"camera": [
|
||||
"medium profile with light streak on edges",
|
||||
"back shot window rim light tracing silhouette",
|
||||
"face close slight smile of relief",
|
||||
"low angle chair wheels reflecting light",
|
||||
"wide airy office, dust motes floating"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "points to sticky notes",
|
||||
"action": "organizes tasks",
|
||||
"camera": [
|
||||
"macro sticky notes with pen glide",
|
||||
"side shot hand gestures casting shadows",
|
||||
"face zoom in purposeful look",
|
||||
"overhead desk layout symmetry",
|
||||
"rack focus foreground notes → him"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "quick call with headset",
|
||||
"action": "nods, types one-handed",
|
||||
"camera": [
|
||||
"face close with soft headset LED glow",
|
||||
"side keyboard shot light skimming keys",
|
||||
"back shot monitor reflections",
|
||||
"low angle desk edge lens flare",
|
||||
"wide office with colleagues as bokeh"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "final keystroke & save",
|
||||
"action": "small fist pump",
|
||||
"camera": [
|
||||
"face zoom in subtle satisfaction",
|
||||
"overhead enter-key press highlight",
|
||||
"side shot progress bar completing",
|
||||
"back shot sun patch moving across floor",
|
||||
"wide with gentle parallax slide"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 4,
|
||||
"lyricAnchor": "bridge",
|
||||
"time": "Noon",
|
||||
"location": "Office meeting room with glass walls",
|
||||
"outfit": "puts blazer back on, pocket square minimal",
|
||||
"lightFX": "top light soft panels, glass reflections, whiteboard glow",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "standing by screen",
|
||||
"action": "presenting calmly",
|
||||
"camera": [
|
||||
"three-quarter presenter with screen bloom",
|
||||
"side shot hand outlining charts",
|
||||
"face zoom in composed tone",
|
||||
"overhead table symmetry + notebooks",
|
||||
"glass reflection two-layer composition"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "listening to feedback",
|
||||
"action": "nods, writes key points",
|
||||
"camera": [
|
||||
"close pen tip gliding with light spark",
|
||||
"profile with colleagues blurred",
|
||||
"face zoom in attentive eyes",
|
||||
"back shot city light filtering",
|
||||
"low angle chair legs + floor sheen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "whiteboard stand",
|
||||
"action": "draws simple plan",
|
||||
"camera": [
|
||||
"over-shoulder marker stroke with squeak",
|
||||
"side shot board light wash",
|
||||
"macro marker cap click",
|
||||
"face zoom in confident smile",
|
||||
"wide room with glass reflections"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "wraps up",
|
||||
"action": "thanks team with small bow",
|
||||
"camera": [
|
||||
"medium group claps, light bloom",
|
||||
"back shot handshake silhouette",
|
||||
"face close gratitude",
|
||||
"overhead chairs and cables geometric",
|
||||
"wide corridor outside glowing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "exits meeting room",
|
||||
"action": "deep breath reset",
|
||||
"camera": [
|
||||
"hallway tracking with specular highlights",
|
||||
"side shot blazer hem flutter",
|
||||
"face zoom in renewed focus",
|
||||
"reflection in glass wall double image",
|
||||
"low angle sun slice across floor"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 5,
|
||||
"lyricAnchor": "bridge",
|
||||
"time": "Early Afternoon",
|
||||
"location": "City park bench under light trees",
|
||||
"outfit": "blazer off, sleeves rolled, relaxed tie-less",
|
||||
"lightFX": "dappled sun through leaves, pollen glitter bokeh, gentle breeze",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "sitting on bench",
|
||||
"action": "unwraps simple sandwich",
|
||||
"camera": [
|
||||
"full body bench shot with leaf bokeh",
|
||||
"side shot paper crinkle detail",
|
||||
"face zoom in serene break",
|
||||
"overhead lunch + watch on wrist",
|
||||
"back shot path with cyclists light streaks"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "takes first bite",
|
||||
"action": "closes eyes a second",
|
||||
"camera": [
|
||||
"close bite with sun rim on cheek",
|
||||
"profile crumbs, shallow DOF",
|
||||
"face zoom in small smile",
|
||||
"wide park pond sparkle",
|
||||
"low angle grass blades glowing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "checks notebook",
|
||||
"action": "jots quick line",
|
||||
"camera": [
|
||||
"macro ink glint",
|
||||
"side shot wrist veins and light",
|
||||
"overhead notebook grid clean",
|
||||
"back shot kite in distant sky",
|
||||
"face close pensive"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "sips water",
|
||||
"action": "exhales, shoulders drop",
|
||||
"camera": [
|
||||
"close water bottle condensation sparkles",
|
||||
"profile sip with sun flare",
|
||||
"wide bench with trees swaying",
|
||||
"ground-level ants-eye leaves",
|
||||
"face gentle relief"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "stands to go",
|
||||
"action": "tucks notebook into bag",
|
||||
"camera": [
|
||||
"back shot dust motes in sun beam",
|
||||
"side shot bag flap catchlight",
|
||||
"low angle steps from gravel",
|
||||
"overhead bench now empty",
|
||||
"wide exit through shimmering heat"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 6,
|
||||
"lyricAnchor": "bridge → rap",
|
||||
"time": "Afternoon",
|
||||
"location": "Open office collaboration zone",
|
||||
"outfit": "smart-casual with blazer on, sleeves half-rolled",
|
||||
"lightFX": "big window shafts, whiteboard bounce, laptop screen glows",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "huddled with team",
|
||||
"action": "points at tablet, laughs",
|
||||
"camera": [
|
||||
"three-quarter group with warm spill",
|
||||
"side shot fingertip reflect on glass",
|
||||
"face zoom in animated eyes",
|
||||
"overhead table, sticky notes constellation",
|
||||
"back shot sun flare between heads"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "pair-programming vibe",
|
||||
"action": "nods at colleague’s suggestion",
|
||||
"camera": [
|
||||
"over-shoulder code lines glow",
|
||||
"profile double-bounce light",
|
||||
"face close appreciative smirk",
|
||||
"wide office as soft bokeh",
|
||||
"macro key tap with sparkle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "high-five moment",
|
||||
"action": "quick celebratory clasp",
|
||||
"camera": [
|
||||
"hand slap in slow-mo micro flare",
|
||||
"wide with cheering silhouettes",
|
||||
"face zoom in spark of pride",
|
||||
"back shot window halo",
|
||||
"low angle chairs and shoe scuffs gleam"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "whiteboard arrows",
|
||||
"action": "draws bold box around goal",
|
||||
"camera": [
|
||||
"marker squeak macro with dust specks",
|
||||
"side shot arm casting graphic shadow",
|
||||
"face determined nod",
|
||||
"overhead board geometry",
|
||||
"wide team nodding"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "break breath",
|
||||
"action": "looks out window to skyline",
|
||||
"camera": [
|
||||
"back shot silhouette and city shimmer",
|
||||
"profile rim light on jawline",
|
||||
"face close hopeful",
|
||||
"low angle blinds lines across suit",
|
||||
"wide room with golden haze creep"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 7,
|
||||
"LyricAnchor": "rap",
|
||||
"time": "Late Afternoon",
|
||||
"location": "Recording studio, vocal booth with pop filter and headphones",
|
||||
"outfit": "black tee under blazer, dark jeans; studio sneakers",
|
||||
"lightFX": "neon edge lights, VU meter glow, soft haze for beams",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "headphones on",
|
||||
"action": "adjusts mic height behind pop filter",
|
||||
"camera": [
|
||||
"face close through pop filter mesh bokeh",
|
||||
"side mic silhouette with LED rim",
|
||||
"overhead booth foam pattern",
|
||||
"back shot cable drape glint",
|
||||
"macro hands rolling knob with light tick"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "eyes closed",
|
||||
"action": "raps first bar with steady breath",
|
||||
"camera": [
|
||||
"profile with breath not hitting mic (good distance)",
|
||||
"face zoom in focused cadence",
|
||||
"VU meter needles dancing",
|
||||
"wide booth with purple-blue haze",
|
||||
"low angle light bar lens flare"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "punch-in moment",
|
||||
"action": "signals engineer, resumes",
|
||||
"camera": [
|
||||
"over-shoulder engineer glass reflection",
|
||||
"side shot finger count-in",
|
||||
"face close confident nod",
|
||||
"macro record light switching on",
|
||||
"wide studio cables lines"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "chorus take",
|
||||
"action": "leans slightly, controlled power",
|
||||
"camera": [
|
||||
"three-quarter with mic halo",
|
||||
"face zoom in grit + hope",
|
||||
"low angle stand and shock mount glint",
|
||||
"back shot waveforms on screen",
|
||||
"overhead booth ceiling star LEDs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "final word",
|
||||
"action": "exhales, half smile",
|
||||
"camera": [
|
||||
"face close de-compress",
|
||||
"side shot headphone slide off one ear",
|
||||
"macro stop button press",
|
||||
"wide studio with color wash fade",
|
||||
"back shot door open to warm hall"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 8,
|
||||
"lyricAnchor": "verse B",
|
||||
"time": "Evening",
|
||||
"location": "City park path with family",
|
||||
"outfit": "soft knit over shirt, dark chinos; family in cozy layers",
|
||||
"lightFX": "golden hour glow, lens flare peeks, fairy-light bokeh",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "walking hand-in-hand",
|
||||
"action": "gentle chat, smile",
|
||||
"camera": [
|
||||
"full body back shot into sun",
|
||||
"side shot swinging hands in flare",
|
||||
"face close warm laugh lines",
|
||||
"overhead trees and light leaks",
|
||||
"low angle shoes kicking leaves sparkle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "kid points at birds",
|
||||
"action": "he kneels to show map",
|
||||
"camera": [
|
||||
"two-shot knee level with soft glow",
|
||||
"face close parental pride",
|
||||
"over-shoulder simple map sketch",
|
||||
"wide path dotted with fairy lights",
|
||||
"macro leaf veins lit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "group selfie",
|
||||
"action": "click with laughter",
|
||||
"camera": [
|
||||
"phone POV smiling faces, sun starburst",
|
||||
"side shot arm extended",
|
||||
"back shot silhouettes + flare arc",
|
||||
"overhead family circle",
|
||||
"wide park twinkle lights on"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "rest on bench",
|
||||
"action": "water break, shared bottle",
|
||||
"camera": [
|
||||
"medium sharing gesture backlit",
|
||||
"face close gentle gratitude",
|
||||
"low angle bottle condensation spark",
|
||||
"overhead bench wood grain glow",
|
||||
"wide with city edge twinkling"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "stand to leave",
|
||||
"action": "quick group hug",
|
||||
"camera": [
|
||||
"tight hug with sun blooming",
|
||||
"side shot hands around shoulders",
|
||||
"back shot long shadows merge",
|
||||
"overhead crown of light around heads",
|
||||
"wide path leading toward skyline"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 9,
|
||||
"lyricAnchor": "verse B",
|
||||
"time": "Night",
|
||||
"location": "Overlook with clean city nightscape",
|
||||
"outfit": "blazer back on, scarf added",
|
||||
"lightFX": "city bokeh, cool-blue sky, subtle haze for star glints",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "hands on railing",
|
||||
"action": "breath visible, quiet awe",
|
||||
"camera": [
|
||||
"full body with skyline glitter",
|
||||
"profile face with neon reflection in eyes",
|
||||
"back shot shoulders squared",
|
||||
"low angle railing shine + lens flare",
|
||||
"overhead city grid lines"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "checks notes on phone",
|
||||
"action": "adds a single word: 'continue'",
|
||||
"camera": [
|
||||
"macro phone screen glow",
|
||||
"side shot finger tap highlight",
|
||||
"face close calm resolve",
|
||||
"wide skyline as bokeh ocean",
|
||||
"back shot scarf ripple"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "exhale slowly",
|
||||
"action": "pockets hands",
|
||||
"camera": [
|
||||
"profile fogged breath in neon halo",
|
||||
"low angle shoe on concrete edge",
|
||||
"face zoom in quiet smile",
|
||||
"overhead subtle city ray streak",
|
||||
"wide with passing light trail"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "turns to camera",
|
||||
"action": "small nod forward",
|
||||
"camera": [
|
||||
"head-on medium with flare arch",
|
||||
"close eyes determined brightness",
|
||||
"side jawline rim light",
|
||||
"back shot then whip-pan reveal city",
|
||||
"wide crane up, city expands"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "steps away",
|
||||
"action": "walks along overlook path",
|
||||
"camera": [
|
||||
"tracking back shot bokeh river",
|
||||
"side shot rail lights passing",
|
||||
"face three-quarter confident",
|
||||
"low angle puddle reflecting skyline",
|
||||
"overhead path curve luminous"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 10,
|
||||
"lyricAnchor": "rap → end",
|
||||
"time": "Night",
|
||||
"location": "Office district after hours",
|
||||
"outfit": "blazer + scarf, gloves optional",
|
||||
"lightFX": "glass facade reflections, wet pavement neon, gentle drizzle mist",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "walking alone between towers",
|
||||
"action": "steady stride, chin up",
|
||||
"camera": [
|
||||
"full body on wet pavement mirror",
|
||||
"side shot droplets catching light",
|
||||
"face close focused calm",
|
||||
"back shot towers converging lines",
|
||||
"overhead umbrella silhouettes distant"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "pauses at intersection",
|
||||
"action": "inhales cool air",
|
||||
"camera": [
|
||||
"low angle traffic light glow on mist",
|
||||
"profile breath plume",
|
||||
"wide glass reflections kaleidoscope",
|
||||
"macro droplet slide on sleeve",
|
||||
"face close half-smile"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "checks tram schedule afar",
|
||||
"action": "decides to walk instead",
|
||||
"camera": [
|
||||
"long lens heat-haze shimmer line",
|
||||
"back shot step away from stop",
|
||||
"side shot rhythmic footfalls",
|
||||
"wide lone figure + city hum",
|
||||
"overhead rain dot glitter"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "crosswalk stride",
|
||||
"action": "bag strap adjustment",
|
||||
"camera": [
|
||||
"ground reflection of zebra stripes",
|
||||
"three-quarter with neon banding",
|
||||
"face close rain specks on cheek",
|
||||
"low angle heel-toe cadence",
|
||||
"wide car lights streak behind"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "turns corner",
|
||||
"action": "street opens to quieter block",
|
||||
"camera": [
|
||||
"back shot disappearing into warm alley glow",
|
||||
"side shot brick wall glisten",
|
||||
"overhead pocket of golden steam vent",
|
||||
"face three-quarter softened by warmth",
|
||||
"wide fade to gentle drizzle veil"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 11,
|
||||
"lyricAnchor": "end",
|
||||
"time": "Late Night",
|
||||
"location": "Home living room, cozy lamp pools",
|
||||
"outfit": "cardigan over tee, soft pants, socks; glasses on table",
|
||||
"lightFX": "warm lamps, TV ambient glow, window rain reflections",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "sinks into sofa",
|
||||
"action": "removes watch, exhales",
|
||||
"camera": [
|
||||
"full body cozy frame with lamp bloom",
|
||||
"macro watch buckle catchlight",
|
||||
"face close relief",
|
||||
"overhead coffee table still life",
|
||||
"back shot curtain rain trails"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "scrolls photos",
|
||||
"action": "smiles at family selfie",
|
||||
"camera": [
|
||||
"close phone screen glow on eyes",
|
||||
"profile soft grin",
|
||||
"wide living room warm pools",
|
||||
"low angle socks on rug texture",
|
||||
"back shot window city twinkle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "leans back",
|
||||
"action": "sips late tea",
|
||||
"camera": [
|
||||
"macro steam ribbon in lamp light",
|
||||
"side shot cup handle highlight",
|
||||
"face close calm closure",
|
||||
"overhead tray symmetry",
|
||||
"wide gentle rack focus to rain"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "turns off lamp",
|
||||
"action": "room dims to TV glow",
|
||||
"camera": [
|
||||
"finger on switch micro flare",
|
||||
"silhouette against window bokeh",
|
||||
"face close eyes soft",
|
||||
"low angle shadows lengthen",
|
||||
"wide night hush settles"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "final glance to window",
|
||||
"action": "small nod—tomorrow again",
|
||||
"camera": [
|
||||
"face three-quarter with faint smile",
|
||||
"back shot rain tracing lines of light",
|
||||
"macro drop racing down glass",
|
||||
"wide living room serene",
|
||||
"fade out on city reflections"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
src/musicspot_generator/fire in the night/song.mp3
Normal file
BIN
src/musicspot_generator/girly girl/face.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
272
src/musicspot_generator/girly girl/scenes.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
src/musicspot_generator/girly girl/song.mp3
Normal file
264
src/musicspot_generator/images.ts
Normal file
@ -0,0 +1,264 @@
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
import { logger } from '../lib/logger';
|
||||
import { callOpenAI } from '../lib/openai';
|
||||
import { callLMStudio } from '../lib/lmstudio';
|
||||
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 || 'zagreb';
|
||||
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 110–140 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
|
||||
|
||||
Important:
|
||||
- adjust the outfit based on the camera framing, describe only what is visible.
|
||||
For example, if the framing is a close-up of the face, do not mention the outfit at all.
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
const IMAGE_PROMPT_PROVIDER = (process.env.IMAGE_PROMPT_PROVIDER || 'lmstudio').toLowerCase();
|
||||
|
||||
async function getImagePromptFromLMStudio(req: string): Promise<string> {
|
||||
const res = await callLMStudio(req);
|
||||
const prompt = res?.imagePrompt || res?.image_prompt || res?.prompt;
|
||||
if (!prompt || typeof prompt !== 'string') {
|
||||
throw new Error('LM Studio failed to return imagePrompt JSON.');
|
||||
}
|
||||
return prompt.trim();
|
||||
}
|
||||
|
||||
async function getImagePrompt(req: string): Promise<string> {
|
||||
if (IMAGE_PROMPT_PROVIDER === 'openai') {
|
||||
return getImagePromptFromOpenAI(req);
|
||||
}
|
||||
// default to LM Studio
|
||||
return getImagePromptFromLMStudio(req);
|
||||
}
|
||||
|
||||
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 getImagePrompt(imgPromptReq);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`${IMAGE_PROMPT_PROVIDER.toUpperCase()} 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);
|
||||
});
|
||||
366
src/musicspot_generator/index.ts
Normal 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 110–140 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 100–140 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);
|
||||
});
|
||||
BIN
src/musicspot_generator/oputstise/face.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
827
src/musicspot_generator/oputstise/scenes.json
Normal 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 bird’s-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 child’s 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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
src/musicspot_generator/oputstise/song.mp3
Normal file
272
src/musicspot_generator/scenes.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
259
src/musicspot_generator/videos.ts
Normal 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 || 'zagreb';
|
||||
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 100–140 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,
|
||||
false,
|
||||
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);
|
||||
});
|
||||
BIN
src/musicspot_generator/zagreb/face.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
834
src/musicspot_generator/zagreb/scenes.json
Normal file
@ -0,0 +1,834 @@
|
||||
{
|
||||
"character": {
|
||||
"bodyType": "average",
|
||||
"hairStyle": "long blonde hair tied in a man bun, white skin, blue eyes, beard"
|
||||
},
|
||||
"scenes": [
|
||||
{
|
||||
"sceneId": 1,
|
||||
"season": "Autumn",
|
||||
"time": "Evening",
|
||||
"location": "Zagreb skyline viewpoint with light fog and warm city lights",
|
||||
"outfit": "dark gray adidas joggers with white side stripes, black heavyweight hoodie (hood down), light quilted autumn jacket in charcoal, white adidas sneakers slightly scuffed, black knit beanie",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "standing at viewpoint railing",
|
||||
"action": "looking over city while exhaling misty breath",
|
||||
"camera": [
|
||||
"fullbody shot with skyline and fog glow",
|
||||
"closeup on eyes and misty breath",
|
||||
"back shot framed by city lights"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "hands in pockets",
|
||||
"action": "nodding head slowly to beat",
|
||||
"camera": [
|
||||
"fullbody shot silhouetted against skyline",
|
||||
"closeup on subtle head nod and beard",
|
||||
"side shot with drifting fog"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "feet near railing edge",
|
||||
"action": "dancing: tight heel-toe shuffle forward and back, ankles loose, arms swinging low in relaxed groove",
|
||||
"camera": [
|
||||
"fullbody shot wide skyline backdrop",
|
||||
"closeup on footwork over wet stone",
|
||||
"side shot tracking along railing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "one arm lifted",
|
||||
"action": "dancing: step-touch left/right with rolling shoulders and head bobs on the snare",
|
||||
"camera": [
|
||||
"fullbody shot low angle, lights behind",
|
||||
"closeup on rolling shoulders and hoodie fabric",
|
||||
"back shot into glowing fog"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "leaning against railing",
|
||||
"action": "staring straight into lens with confident half-smile",
|
||||
"camera": [
|
||||
"fullbody shot framed by rail",
|
||||
"closeup on confident stare",
|
||||
"side shot with fog drift"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 2,
|
||||
"season": "Autumn",
|
||||
"time": "Day",
|
||||
"location": "Inside Zagreb blue tram, sunlight streaks through windows",
|
||||
"outfit": "mid-blue jeans, white cotton t-shirt under light gray zip hoodie, navy bomber jacket, clean white adidas sneakers, simple black wristwatch",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "sitting by window",
|
||||
"action": "writing lyrics in notebook as city blurs past",
|
||||
"camera": [
|
||||
"fullbody shot seated by window",
|
||||
"closeup on pen and notebook",
|
||||
"side shot with city motion streaks"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "standing near door holding rail",
|
||||
"action": "dancing: rail-hold groove with shoulder ticks and toe taps in sync with tram sway",
|
||||
"camera": [
|
||||
"fullbody shot vertical down tram aisle",
|
||||
"closeup on toe taps on rubber floor",
|
||||
"side shot from opposite pole"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "leaning on glass",
|
||||
"action": "fogging window with breath then drawing a small heart",
|
||||
"camera": [
|
||||
"fullbody shot in window bay",
|
||||
"closeup on fingertip tracing heart",
|
||||
"side shot with sunlight flare"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "feet apart, both hands on strap",
|
||||
"action": "dancing: micro two-step in place with chest pops on backbeat",
|
||||
"camera": [
|
||||
"fullbody shot in aisle",
|
||||
"closeup on chest pops through hoodie",
|
||||
"back shot down toward exit door"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "stepping off tram",
|
||||
"action": "placing foot on wet pavement and glancing up",
|
||||
"camera": [
|
||||
"fullbody shot exiting with tram behind",
|
||||
"closeup on sneaker hitting puddle",
|
||||
"side shot tracking walk away"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 3,
|
||||
"season": "Autumn",
|
||||
"time": "Morning",
|
||||
"location": "Dolac market with red umbrellas, crisp air",
|
||||
"outfit": "black adidas joggers, cream knit t-shirt under a denim trucker jacket, white adidas sneakers with gray socks, fingerless knit gloves",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "walking through market",
|
||||
"action": "nodding to vendors and smiling",
|
||||
"camera": [
|
||||
"fullbody shot weaving among umbrellas",
|
||||
"closeup on smile and beard",
|
||||
"side shot with shoppers blurred"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "open space between stalls",
|
||||
"action": "dancing: pivot-spin on heel, one arm slicing air, stopping on beat",
|
||||
"camera": [
|
||||
"fullbody shot centered under umbrellas",
|
||||
"closeup on pivoting sneaker",
|
||||
"back shot with umbrellas radiating"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "holding an apple",
|
||||
"action": "taking a bite and chuckling",
|
||||
"camera": [
|
||||
"fullbody shot framed by produce colors",
|
||||
"closeup on bite and beard",
|
||||
"side shot with vendor in bokeh"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "near stall edge",
|
||||
"action": "dancing: side-step bounce with finger snaps and quick shoulder check",
|
||||
"camera": [
|
||||
"fullbody shot on cobblestone",
|
||||
"closeup on finger snaps",
|
||||
"side shot past crates of apples"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "sitting on bench",
|
||||
"action": "exhaling a long misty breath up to sky",
|
||||
"camera": [
|
||||
"fullbody shot bench among umbrellas",
|
||||
"closeup on breath plume",
|
||||
"back shot to morning light"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 4,
|
||||
"season": "Autumn",
|
||||
"time": "Afternoon",
|
||||
"location": "Graffiti alley glowing with neon tubes and hanging fog",
|
||||
"outfit": "charcoal adidas joggers, navy heavyweight hoodie layered under a black denim jacket, white adidas sneakers worn at toes, charcoal beanie, silver chain",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "line with two friends",
|
||||
"action": "dancing: synchronized stomp-stomp-slide combo with shoulder hits (together)",
|
||||
"camera": [
|
||||
"fullbody shot of trio in wide alley",
|
||||
"closeup on synchronized sneakers",
|
||||
"side shot panning across the line"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "center of small circle",
|
||||
"action": "dancing: knee spin to floor to knee up while friends clap and hype (together focus on him)",
|
||||
"camera": [
|
||||
"fullbody shot near ground level",
|
||||
"closeup on balancing palm and sleeve",
|
||||
"back shot showing friends clapping"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "leaning on wall",
|
||||
"action": "rapping directly to camera, neon flicker behind",
|
||||
"camera": [
|
||||
"fullbody shot against graffiti panel",
|
||||
"closeup on lips and breath in cold air",
|
||||
"side shot with neon reflections"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "shoulder to shoulder with friends",
|
||||
"action": "dancing: side-step shuffle with hand waves and head whips (together)",
|
||||
"camera": [
|
||||
"fullbody shot three-wide groove",
|
||||
"closeup on hand waves across frame",
|
||||
"back shot down glowing alley"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "arms stretched open",
|
||||
"action": "looking up as fog lifts around lights",
|
||||
"camera": [
|
||||
"fullbody shot silhouette in neon haze",
|
||||
"closeup on eyes lit by tubes",
|
||||
"side shot past a painted mural"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 5,
|
||||
"season": "Autumn",
|
||||
"time": "Evening",
|
||||
"location": "Sava riverbank at golden hour with low mist",
|
||||
"outfit": "mid-wash jeans, black long-sleeve tee under olive lightweight parka, white adidas sneakers with darker laces, gray knit scarf",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "walking riverside",
|
||||
"action": "dancing: loose arm swing with heel peels and forward glide",
|
||||
"camera": [
|
||||
"fullbody shot along river path",
|
||||
"closeup on heel peel on damp gravel",
|
||||
"side shot with river reflections"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "hands in parka pockets",
|
||||
"action": "gazing at sun reflecting on water",
|
||||
"camera": [
|
||||
"fullbody shot silhouette at bank",
|
||||
"closeup on eyes catching gold",
|
||||
"back shot toward low sun"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "standing by railing",
|
||||
"action": "tapping rhythm on rail with knuckles",
|
||||
"camera": [
|
||||
"fullbody shot framed by rail lines",
|
||||
"closeup on tapping knuckles",
|
||||
"side shot with mist ribboning"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "open space at water edge",
|
||||
"action": "dancing: slide-to-cross step with chest pop accent and arm sweep",
|
||||
"camera": [
|
||||
"fullbody shot river behind",
|
||||
"closeup on chest pop under tee",
|
||||
"side shot tracking along water"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "head tilted back",
|
||||
"action": "breathing out slowly, eyes closed",
|
||||
"camera": [
|
||||
"fullbody shot with fading sun",
|
||||
"closeup on breath plume dispersing",
|
||||
"back shot to amber sky"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 6,
|
||||
"season": "Autumn",
|
||||
"time": "Night",
|
||||
"location": "Ban Jelačić Square with neon ads and light fog",
|
||||
"outfit": "black adidas joggers, gray t-shirt, slate bomber jacket, white adidas sneakers, fingerless gloves",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "group line facing camera",
|
||||
"action": "dancing: bounce-bounce-step pattern with knee lifts in unison (together)",
|
||||
"camera": [
|
||||
"fullbody shot of group centered in square",
|
||||
"closeup on synchronized knees",
|
||||
"side shot with neon bokeh"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "front of dance circle",
|
||||
"action": "dancing: spin in, arms open, stop on beat with chest hit (together feature)",
|
||||
"camera": [
|
||||
"fullbody shot in circle opening",
|
||||
"closeup on arm extension and jacket fold",
|
||||
"back shot toward statue and lights"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "friends behind clapping",
|
||||
"action": "rapping at lens with fog swirling",
|
||||
"camera": [
|
||||
"fullbody shot square tiles reflecting",
|
||||
"closeup on lips and beard",
|
||||
"side shot past cheering friends"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "two friends flanking",
|
||||
"action": "dancing: traveling shuffle diagonally with toe digs and head tilts (together)",
|
||||
"camera": [
|
||||
"fullbody shot three-across traveling",
|
||||
"closeup on toe digs kicking sparks of water",
|
||||
"back shot crossing square"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "hands on hips",
|
||||
"action": "smiling up at neon glow",
|
||||
"camera": [
|
||||
"fullbody shot slight low angle",
|
||||
"closeup on smiling eyes",
|
||||
"side shot with tram passing blur"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 7,
|
||||
"season": "Autumn",
|
||||
"time": "Day",
|
||||
"location": "Corner café with fogged windows and warm Edison bulbs",
|
||||
"outfit": "dark jeans, soft oatmeal t-shirt, forest-green hoodie under tan canvas jacket, white adidas sneakers, wool cap",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "seated with two friends at table",
|
||||
"action": "dancing: table-top beat — finger drumming while shoulders groove (together)",
|
||||
"camera": [
|
||||
"fullbody shot across small table",
|
||||
"closeup on finger rhythms on wood",
|
||||
"side shot with fogged window"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "standing by chair facing friend",
|
||||
"action": "dancing: mirrored mini two-step and clap on two (together)",
|
||||
"camera": [
|
||||
"fullbody shot of paired groove",
|
||||
"closeup on clapping hands",
|
||||
"back shot toward bar lights"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "holding coffee cup",
|
||||
"action": "blowing steam off cup and smirking",
|
||||
"camera": [
|
||||
"fullbody shot seated relaxed",
|
||||
"closeup on steam curling",
|
||||
"side shot with bulbs in bokeh"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "friends around hyping",
|
||||
"action": "dancing: quick spin ending in finger point to camera (together energy)",
|
||||
"camera": [
|
||||
"fullbody shot between tables",
|
||||
"closeup on finger point and grin",
|
||||
"back shot past other patrons"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "leaning back in chair",
|
||||
"action": "exhaling lightly, calm confidence",
|
||||
"camera": [
|
||||
"fullbody shot reclined posture",
|
||||
"closeup on relaxed eyes",
|
||||
"side shot across window condensation"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 8,
|
||||
"season": "Autumn",
|
||||
"time": "Evening",
|
||||
"location": "Old Zagreb apartment balcony with laundry lines and city fog",
|
||||
"outfit": "black adidas joggers, slate long-sleeve t-shirt under navy quilted vest, white adidas sneakers, knit scarf",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "leaning on rail under warm bulb",
|
||||
"action": "writing lyric line in notebook",
|
||||
"camera": [
|
||||
"fullbody shot balcony edge",
|
||||
"closeup on pen and paper",
|
||||
"side shot past swaying laundry"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "hands lifted",
|
||||
"action": "dancing: shoulder rolls with slow side travel, wrists loose",
|
||||
"camera": [
|
||||
"fullbody shot city behind",
|
||||
"closeup on rolling shoulders",
|
||||
"back shot toward foggy rooftops"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "center balcony tile",
|
||||
"action": "dancing: foot slide-cross with quick head nod accents",
|
||||
"camera": [
|
||||
"fullbody shot top-down slight",
|
||||
"closeup on sliding sneakers on tiles",
|
||||
"side shot along rail"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "headphones on",
|
||||
"action": "nodding in time, eyes closed",
|
||||
"camera": [
|
||||
"fullbody shot under balcony lamp",
|
||||
"closeup on serene face and beard",
|
||||
"back shot to dim streetlights"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "sitting on chair",
|
||||
"action": "long breath into cold night air",
|
||||
"camera": [
|
||||
"fullbody shot with chair legs angled",
|
||||
"closeup on breath plume",
|
||||
"side shot from doorway"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 9,
|
||||
"season": "Autumn",
|
||||
"time": "Day",
|
||||
"location": "Neighborhood street football court, leaves on ground",
|
||||
"outfit": "navy tracksuit top, black adidas joggers, white adidas sneakers with turf dust, simple beanie",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "triangle with friends",
|
||||
"action": "passing football quickly between feet (together)",
|
||||
"camera": [
|
||||
"fullbody shot wide half-court",
|
||||
"closeup on ball taps and passes",
|
||||
"side shot through metal fence"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "center circle",
|
||||
"action": "dancing: soccer-shuffle — toe taps on ball with hip sway (together)",
|
||||
"camera": [
|
||||
"fullbody shot center circle",
|
||||
"closeup on rapid toe taps",
|
||||
"back shot with friends cheering"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "near goalpost",
|
||||
"action": "dancing: step-over fake into spin and pose (solo flair)",
|
||||
"camera": [
|
||||
"fullbody shot near net",
|
||||
"closeup on foot feint over ball",
|
||||
"side shot across goal mesh"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "leaning on post",
|
||||
"action": "chuckling at camera, breath visible",
|
||||
"camera": [
|
||||
"fullbody shot framed by net",
|
||||
"closeup on smile and breath",
|
||||
"back shot to autumn trees"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "group line on sideline",
|
||||
"action": "dancing: circle footwork — alternating shuffles in and out (together)",
|
||||
"camera": [
|
||||
"fullbody shot line routine",
|
||||
"closeup on synchronized steps",
|
||||
"side shot along chalk line"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 10,
|
||||
"season": "Autumn",
|
||||
"time": "Night",
|
||||
"location": "Rainy cobblestone street with neon reflections",
|
||||
"outfit": "black jeans, charcoal hoodie with hood up, water-resistant black shell jacket, white adidas sneakers wet with rain",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "mid-street",
|
||||
"action": "kicking puddle to splash arcs of neon water",
|
||||
"camera": [
|
||||
"fullbody shot centered on reflections",
|
||||
"closeup on splash around sneaker",
|
||||
"back shot with neon signs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "feet planted wide",
|
||||
"action": "dancing: chest pulse on beat with slow head roll, rain dripping off brim",
|
||||
"camera": [
|
||||
"fullbody shot silhouette in rain",
|
||||
"closeup on chest pulse through hoodie",
|
||||
"side shot with water streaks"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "arms open",
|
||||
"action": "letting rain hit face, eyes closed",
|
||||
"camera": [
|
||||
"fullbody shot T-pose slight",
|
||||
"closeup on raindrops on beard",
|
||||
"back shot with street glow"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "pivot foot ready",
|
||||
"action": "dancing: 360° rain spin with heel skid and jacket flare",
|
||||
"camera": [
|
||||
"fullbody shot tracking spin",
|
||||
"closeup on heel skid and spray",
|
||||
"side shot following arc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "hood low",
|
||||
"action": "staring into lens, breath steaming",
|
||||
"camera": [
|
||||
"fullbody shot still in rain",
|
||||
"closeup on intense eyes",
|
||||
"back shot with wet cobbles"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 11,
|
||||
"season": "Autumn",
|
||||
"time": "Morning",
|
||||
"location": "Zagreb Cathedral steps in pale fog",
|
||||
"outfit": "dark jeans, black crew tee, gray wool overcoat, white adidas sneakers, knit gloves",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "sitting mid-steps",
|
||||
"action": "writing lyrics steadily",
|
||||
"camera": [
|
||||
"fullbody shot wide steps and spires",
|
||||
"closeup on pen strokes",
|
||||
"side shot with drifting fog"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "standing arms open",
|
||||
"action": "exhaling long breath to sky",
|
||||
"camera": [
|
||||
"fullbody shot facing façade",
|
||||
"closeup on breath plume",
|
||||
"back shot to gothic towers"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "cleared area on steps",
|
||||
"action": "dancing: step-tap pattern up two steps, back down, snap on beat",
|
||||
"camera": [
|
||||
"fullbody shot across steps",
|
||||
"closeup on step taps",
|
||||
"side shot along handrail"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "headphones on",
|
||||
"action": "slow nod timing to kick and snare",
|
||||
"camera": [
|
||||
"fullbody shot centered on landing",
|
||||
"closeup on nod and beard",
|
||||
"back shot to open square"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "hands in coat pockets",
|
||||
"action": "looking up the tower silently",
|
||||
"camera": [
|
||||
"fullbody shot low angle",
|
||||
"closeup on upward gaze",
|
||||
"side shot with stone texture"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 12,
|
||||
"season": "Autumn",
|
||||
"time": "Night",
|
||||
"location": "Lantern-lit garden path with candle clusters and light ground fog",
|
||||
"outfit": "black adidas joggers, charcoal long-sleeve tee, midnight blue bomber, white adidas sneakers catching candle glints",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "walking along lantern path",
|
||||
"action": "lifting a lantern and holding it forward",
|
||||
"camera": [
|
||||
"fullbody shot through lantern aisle",
|
||||
"closeup on lantern glow on face",
|
||||
"back shot with lantern trail"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "small clearing",
|
||||
"action": "dancing: lantern in left hand, slow side-glide with soft wrist circles",
|
||||
"camera": [
|
||||
"fullbody shot framed by candles",
|
||||
"closeup on circling wrist and flame",
|
||||
"side shot across floating fog"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "at small garden table",
|
||||
"action": "resting chin on hands, dreamy stare",
|
||||
"camera": [
|
||||
"fullbody shot seated with lantern foreground",
|
||||
"closeup on gentle eyes",
|
||||
"back shot to candle clusters"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "beneath hanging lights",
|
||||
"action": "dancing: slow turn with arm rise, ending in still pose under lights",
|
||||
"camera": [
|
||||
"fullbody shot low angle to lights",
|
||||
"closeup on raised palm",
|
||||
"side shot passing candle bokeh"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "bare path",
|
||||
"action": "spinning lightly as jacket hem flows",
|
||||
"camera": [
|
||||
"fullbody shot tracking spin",
|
||||
"closeup on jacket swirl",
|
||||
"back shot retreating down path"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 13,
|
||||
"season": "Autumn",
|
||||
"time": "Evening",
|
||||
"location": "Warm recording studio with wooden panels, microphone on stand with pop filter, soft amber lamps",
|
||||
"outfit": "charcoal adidas joggers, black heavyweight t-shirt, dark zip hoodie open at chest, studio headphones, white adidas sneakers",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "standing at mic, one hand on headphones",
|
||||
"action": "singing through pop filter with eyes closed",
|
||||
"camera": [
|
||||
"fullbody shot booth and panels",
|
||||
"closeup on lips behind pop filter fabric",
|
||||
"side shot across mic arm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "one hand gripping mic stand",
|
||||
"action": "dancing: subtle knee bounce and torso sway while holding pitch",
|
||||
"camera": [
|
||||
"fullbody shot between panels",
|
||||
"closeup on torso sway and hoodie folds",
|
||||
"back shot from behind pop filter"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "leaning slightly toward mic",
|
||||
"action": "soft smile between lines, breath visible",
|
||||
"camera": [
|
||||
"fullbody shot centered on stand",
|
||||
"closeup on smile and beard texture",
|
||||
"side shot with amber lamp flare"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "adjusting headphones",
|
||||
"action": "dancing: micro step-touch in place with shoulder ticks as chorus hits",
|
||||
"camera": [
|
||||
"fullbody shot framed by acoustic panels",
|
||||
"closeup on headphone cup and hand",
|
||||
"back shot over shoulder into booth glass"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "stool behind mic",
|
||||
"action": "holding lyric sheet steady, one line delivered",
|
||||
"camera": [
|
||||
"fullbody shot seated by stand",
|
||||
"closeup on lyric sheet print",
|
||||
"side shot past pop filter ring"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 14,
|
||||
"season": "Autumn",
|
||||
"time": "Day",
|
||||
"location": "Minimalist white studio booth, diffused panels, microphone with pop filter",
|
||||
"outfit": "light gray adidas joggers, crisp white t-shirt, soft white zip hoodie, clean white adidas sneakers, over-ear studio headphones",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "sitting on white stool at mic",
|
||||
"action": "singing gently into pop filter with steady breath",
|
||||
"camera": [
|
||||
"fullbody shot bright white booth",
|
||||
"closeup on mouth behind pop fabric",
|
||||
"side shot along mic arm and cable"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "standing, lyric sheet in left hand",
|
||||
"action": "dancing: soft sway with sheet hand marking the beat while delivering a phrase",
|
||||
"camera": [
|
||||
"fullbody shot against white wall",
|
||||
"closeup on sheet hand pulsing beat",
|
||||
"back shot to frosted panel glow"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "head slightly tilted",
|
||||
"action": "smiling at the take, exhale visible in cool booth",
|
||||
"camera": [
|
||||
"fullbody shot centered in booth",
|
||||
"closeup on gentle smile",
|
||||
"side shot with panel edge parallax"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "hands lightly on headphones",
|
||||
"action": "dancing: tiny step-rock left-right, shoulders ticking to metronome",
|
||||
"camera": [
|
||||
"fullbody shot with mic foreground",
|
||||
"closeup on headphone cushions and fingers",
|
||||
"back shot over shoulder to pop filter"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "one hand on mic stand",
|
||||
"action": "holding final note clean through pop filter",
|
||||
"camera": [
|
||||
"fullbody shot with stand and cable",
|
||||
"closeup on pop filter mesh catching light",
|
||||
"side shot from booth door"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||