save changes
This commit is contained in:
BIN
src/musicspot_generator/fire in the night/face.png
Normal file
BIN
src/musicspot_generator/fire in the night/face.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
830
src/musicspot_generator/fire in the night/scenes.json
Normal file
830
src/musicspot_generator/fire in the night/scenes.json
Normal file
@ -0,0 +1,830 @@
|
||||
{
|
||||
"song": {
|
||||
"title": "Crimson Shadows",
|
||||
"artist": "Nocturna",
|
||||
"genre": "Gothic Metal",
|
||||
"mood": "Dark, powerful, dramatic"
|
||||
},
|
||||
"character": {
|
||||
"bodyType": "slim yet strong presence",
|
||||
"hairStyle": "long black hair with red streaks, slightly messy"
|
||||
},
|
||||
"scenes": [
|
||||
{
|
||||
"sceneId": 1,
|
||||
"time": "Night",
|
||||
"location": "Ballroom with broken chandeliers and candles",
|
||||
"outfit": "white gothic ball gown with lace sleeves",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "standing tall in the center of the ballroom",
|
||||
"action": "raising arms dramatically",
|
||||
"camera": [
|
||||
"full body shot with chandeliers above",
|
||||
"low angle from cracked floor",
|
||||
"overhead view of gown spreading",
|
||||
"face zoom in on serious look",
|
||||
"back shot showing gown trail"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "leaning back scream",
|
||||
"action": "shouting with wide-open mouth",
|
||||
"camera": [
|
||||
"face zoom in during scream",
|
||||
"low angle emphasizing mouth open",
|
||||
"side shot with candlelight shadows",
|
||||
"zoom in to part of body trembling hand",
|
||||
"overhead shot capturing scream posture"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "walking slowly",
|
||||
"action": "dragging fingers on curtain",
|
||||
"camera": [
|
||||
"back shot trailing gown",
|
||||
"side shot of hand on velvet",
|
||||
"full body shot from distance",
|
||||
"face zoom in on haunted gaze",
|
||||
"low angle with curtain shadows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "kneeling on broken tiles",
|
||||
"action": "holding necklace tightly",
|
||||
"camera": [
|
||||
"overhead shot of kneeling pose",
|
||||
"face zoom in with tears",
|
||||
"low angle from shattered floor",
|
||||
"side shot framing necklace",
|
||||
"back shot with gown spread"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "spinning mid-gown flare",
|
||||
"action": "screaming with wide-open mouth mid-spin",
|
||||
"camera": [
|
||||
"face zoom in with hair flying",
|
||||
"low angle capturing spin",
|
||||
"overhead highlighting gown swirl",
|
||||
"zoom in to mouth open",
|
||||
"side shot with candle flames shaking"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 2,
|
||||
"time": "Night",
|
||||
"location": "Dark graveyard with blood moon",
|
||||
"outfit": "red gothic dress with corset and veil",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "standing among gravestones",
|
||||
"action": "lifting veil slowly",
|
||||
"camera": [
|
||||
"full body shot with gravestones",
|
||||
"face zoom in through veil",
|
||||
"overhead shot with moonlight",
|
||||
"side shot with crosses behind",
|
||||
"back shot with veil flowing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "bending forward toward the sky",
|
||||
"action": "shouting with wide-open mouth at the blood moon",
|
||||
"camera": [
|
||||
"face zoom in from below",
|
||||
"low angle with moon glow",
|
||||
"overhead capturing scream to sky",
|
||||
"side shot emphasizing mouth open",
|
||||
"zoom in to part of body trembling hands"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "kneeling by gravestone",
|
||||
"action": "touching carved letters",
|
||||
"camera": [
|
||||
"back shot with gravestone silhouette",
|
||||
"low angle with fog rising",
|
||||
"face zoom in teary",
|
||||
"overhead with candle accents",
|
||||
"side shot of hand on stone"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "walking past tombs",
|
||||
"action": "dragging gown through dirt",
|
||||
"camera": [
|
||||
"full body wide shot",
|
||||
"back shot with gown trail",
|
||||
"overhead with drifting fog",
|
||||
"side shot framing gravestones",
|
||||
"zoom in to part of body—bleeding palm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "clutching chest in anguish",
|
||||
"action": "screaming again with wide-open mouth",
|
||||
"camera": [
|
||||
"face zoom in intense",
|
||||
"low angle shaky frame",
|
||||
"overhead gown spread",
|
||||
"side shot with blood moon glow",
|
||||
"zoom in to part of body—trembling mouth"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 3,
|
||||
"time": "Night",
|
||||
"location": "Castle corridor with torches",
|
||||
"outfit": "black velvet gothic gown with silver accents",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "walking slowly through corridor",
|
||||
"action": "dragging hand across stone wall",
|
||||
"camera": [
|
||||
"back shot trailing behind",
|
||||
"side shot hand scraping the wall",
|
||||
"face zoom in haunted eyes",
|
||||
"overhead torches flickering",
|
||||
"full body wide frame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "standing between torches, head tilted back",
|
||||
"action": "shouting with wide-open mouth",
|
||||
"camera": [
|
||||
"low angle torch flames",
|
||||
"face zoom in mouth open",
|
||||
"overhead capturing hall symmetry",
|
||||
"side shot framing torches",
|
||||
"zoom in to part of body—clenched fists"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "kneeling near cracked stone",
|
||||
"action": "holding broken goblet",
|
||||
"camera": [
|
||||
"overhead spilled goblet and shards",
|
||||
"side shot with long shadow",
|
||||
"face zoom in tragic expression",
|
||||
"back shot of corridor depth",
|
||||
"low angle torch flicker"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "standing with hand raised",
|
||||
"action": "reaching toward ceiling arches",
|
||||
"camera": [
|
||||
"full body shot angled upward",
|
||||
"low angle showing height of arches",
|
||||
"face zoom in determination",
|
||||
"back shot framed by arch",
|
||||
"overhead with soft torch glow"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "arched back against wall",
|
||||
"action": "screaming again with wide-open mouth",
|
||||
"camera": [
|
||||
"side shot hair flowing",
|
||||
"face zoom in mouth open",
|
||||
"low angle wall cracks",
|
||||
"back shot dramatic shadow",
|
||||
"overhead dim torch light"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 4,
|
||||
"time": "Night",
|
||||
"location": "Castle rooftop under stars and aurora",
|
||||
"outfit": "red flowing gothic gown with crimson sash",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "standing on rooftop edge",
|
||||
"action": "arms spread to the sky",
|
||||
"camera": [
|
||||
"full body shot with starscape",
|
||||
"overhead aurora curtains",
|
||||
"face zoom in serene gaze",
|
||||
"back shot with gown streaming",
|
||||
"low angle with night sky dome"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "kneeling on cold stone",
|
||||
"action": "shouting with wide-open mouth into the wind",
|
||||
"camera": [
|
||||
"face zoom in under stars",
|
||||
"overhead aurora swirl around",
|
||||
"side shot mouth open profile",
|
||||
"low angle wind tugging fabric",
|
||||
"zoom in to part of body—trembling hand on stone"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "walking slowly along the ledge",
|
||||
"action": "dragging gown across stone",
|
||||
"camera": [
|
||||
"back shot trailing fabric",
|
||||
"side shot with aurora behind",
|
||||
"face zoom in sorrow",
|
||||
"overhead glinting stars",
|
||||
"full body wide rooftop span"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "sitting on the ledge",
|
||||
"action": "staring at the blood moon",
|
||||
"camera": [
|
||||
"face zoom in pale moonlight",
|
||||
"side shot moon edge glow",
|
||||
"back shot open horizon",
|
||||
"overhead framing gown spread",
|
||||
"low angle subtle lens flare"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "arched back with hair whipping",
|
||||
"action": "screaming again with wide-open mouth",
|
||||
"camera": [
|
||||
"face zoom in intensity",
|
||||
"side shot mouth open",
|
||||
"low angle aurora painted sky",
|
||||
"overhead moon halo",
|
||||
"back shot wind-rushed hair"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 5,
|
||||
"time": "Night",
|
||||
"location": "Cathedral interior with neon red lamps",
|
||||
"outfit": "white gothic lace dress with spiked choker",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "standing at altar",
|
||||
"action": "raising arms slowly",
|
||||
"camera": [
|
||||
"full body altar frame",
|
||||
"low angle stained glass glow",
|
||||
"overhead neon lamps",
|
||||
"face zoom in cold eyes",
|
||||
"side shot candle rows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "in front of the altar",
|
||||
"action": "shouting with wide-open mouth",
|
||||
"camera": [
|
||||
"face zoom in wide scream",
|
||||
"side shot neon wash",
|
||||
"low angle altar cross",
|
||||
"overhead long shadows",
|
||||
"zoom in to part of body—tensed throat"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "kneeling in prayer",
|
||||
"action": "clutching rosary tightly",
|
||||
"camera": [
|
||||
"back shot neon cross silhouette",
|
||||
"face zoom in closed eyes",
|
||||
"overhead subtle aura",
|
||||
"side shot trembling fingers",
|
||||
"low angle pew shadows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "walking through pews",
|
||||
"action": "gown brushing wood",
|
||||
"camera": [
|
||||
"full body wide aisle",
|
||||
"back shot trailing fabric",
|
||||
"overhead shadow mosaic",
|
||||
"face zoom in haunted look",
|
||||
"side shot cracked pew ends"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "kneeling backward",
|
||||
"action": "screaming again with wide-open mouth",
|
||||
"camera": [
|
||||
"face zoom in strained",
|
||||
"overhead neon cross glow",
|
||||
"side shot mouth open profile",
|
||||
"low angle altar frame",
|
||||
"zoom in to part of body—open mouth"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 6,
|
||||
"time": "Night",
|
||||
"location": "Dungeon with chains and damp stone",
|
||||
"outfit": "black gothic leather gown with lace gloves",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "standing near dangling chains",
|
||||
"action": "touching cold iron",
|
||||
"camera": [
|
||||
"face zoom in fogged breath",
|
||||
"side shot hand on chain",
|
||||
"back shot against damp wall",
|
||||
"low angle shadows of chains",
|
||||
"overhead dim torch circle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "head thrown back",
|
||||
"action": "shouting with wide-open mouth while gripping chains",
|
||||
"camera": [
|
||||
"face zoom in raw scream",
|
||||
"side shot taut chains",
|
||||
"low angle wall fissures",
|
||||
"overhead claustrophobic frame",
|
||||
"zoom in to part of body—mouth close"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "kneeling, wrists lifted",
|
||||
"action": "pulling against shackles",
|
||||
"camera": [
|
||||
"face zoom in wet lashes",
|
||||
"side shot trembling wrists",
|
||||
"back shot chain stretch",
|
||||
"overhead square of light",
|
||||
"low angle torch sputter"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "standing and straining",
|
||||
"action": "yanking chains from ring",
|
||||
"camera": [
|
||||
"full body struggle shot",
|
||||
"side shot metal tension",
|
||||
"back shot elongated shadow",
|
||||
"face zoom in fury",
|
||||
"overhead dust falling"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "arched back, wrists high",
|
||||
"action": "screaming again with wide-open mouth",
|
||||
"camera": [
|
||||
"face zoom in contorted scream",
|
||||
"side shot mouth open",
|
||||
"overhead chains rattling",
|
||||
"low angle shadow clawing up wall",
|
||||
"zoom in to part of body—throat close"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 7,
|
||||
"time": "Morning",
|
||||
"location": "Girly bedroom with dark lace curtains",
|
||||
"outfit": "black pleated mini skirt, oversized white shirt, ripped stockings",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "sitting on bed cross-legged",
|
||||
"action": "playing with necklace charm",
|
||||
"camera": [
|
||||
"full body shot on bed",
|
||||
"side shot necklace in hand",
|
||||
"face zoom in subtle smile",
|
||||
"overhead quilt folds",
|
||||
"back shot window light through lace"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "leaning back over pillows",
|
||||
"action": "shouting with wide-open mouth",
|
||||
"camera": [
|
||||
"face zoom in open mouth",
|
||||
"side shot messy hair",
|
||||
"low angle bed frame edge",
|
||||
"overhead scream posture",
|
||||
"zoom in to part of body—trembling hand"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "lying sideways",
|
||||
"action": "looking softly at camera",
|
||||
"camera": [
|
||||
"face zoom in gentle eyes",
|
||||
"side shot pillow texture",
|
||||
"overhead relaxed pose",
|
||||
"back shot lace curtain glow",
|
||||
"full body bed perspective"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "standing by window",
|
||||
"action": "touching glass",
|
||||
"camera": [
|
||||
"face zoom in window reflection",
|
||||
"side shot pale fingers on glass",
|
||||
"overhead curtain shadow pattern",
|
||||
"back shot from doorway",
|
||||
"low angle diffused light"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "kneeling on bed",
|
||||
"action": "screaming again with wide-open mouth",
|
||||
"camera": [
|
||||
"face zoom in scream",
|
||||
"side shot mouth open profile",
|
||||
"low angle trembling posture",
|
||||
"overhead tangled sheets",
|
||||
"zoom in to part of body—throat close"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 8,
|
||||
"time": "Night",
|
||||
"location": "Urban street with neon lights and wet pavement",
|
||||
"outfit": "red tartan skirt, black crop top, fishnet stockings",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "walking on crosswalk",
|
||||
"action": "flicking hair",
|
||||
"camera": [
|
||||
"full body wide crosswalk",
|
||||
"side shot neon reflections",
|
||||
"face zoom in smirk",
|
||||
"back shot traffic lights",
|
||||
"overhead city glow grid"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "standing under neon sign",
|
||||
"action": "shouting with wide-open mouth",
|
||||
"camera": [
|
||||
"face zoom in neon bloom",
|
||||
"side shot mouth open",
|
||||
"low angle billboard glow",
|
||||
"overhead zebra stripes",
|
||||
"zoom in to part of body—clenched fists"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "leaning on graffiti wall",
|
||||
"action": "crossing arms with attitude",
|
||||
"camera": [
|
||||
"side shot tag texture",
|
||||
"face zoom in cool gaze",
|
||||
"back shot city night bokeh",
|
||||
"overhead shadow frame",
|
||||
"full body casual stance"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "kneeling on sidewalk",
|
||||
"action": "tracing chalk graffiti",
|
||||
"camera": [
|
||||
"overhead chalk lines",
|
||||
"side shot hand drawing",
|
||||
"face zoom in focused",
|
||||
"back shot wet pavement shimmer",
|
||||
"low angle neon bleed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "jumping mid-beat",
|
||||
"action": "screaming again with wide-open mouth in mid-air",
|
||||
"camera": [
|
||||
"face zoom in scream",
|
||||
"side shot mouth open",
|
||||
"overhead body frozen mid-jump",
|
||||
"back shot neon flare",
|
||||
"zoom in to part of body—throat close"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 9,
|
||||
"time": "Afternoon",
|
||||
"location": "Park bench under autumn trees",
|
||||
"outfit": "black floral dress with lace stockings and boots",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "sitting on bench",
|
||||
"action": "looking down thoughtfully",
|
||||
"camera": [
|
||||
"full body bench shot",
|
||||
"side shot fallen leaves",
|
||||
"face zoom in pensive gaze",
|
||||
"overhead bench frame",
|
||||
"back shot winding path"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "standing in leaf fall",
|
||||
"action": "shouting with wide-open mouth to the sky",
|
||||
"camera": [
|
||||
"face zoom in scream",
|
||||
"side shot mouth open",
|
||||
"low angle tree branches",
|
||||
"overhead spinning leaves",
|
||||
"zoom in to part of body—trembling hands"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "lying on grass",
|
||||
"action": "picking petals from a flower",
|
||||
"camera": [
|
||||
"overhead flower detail",
|
||||
"face zoom in soft smile",
|
||||
"side shot fingers on petals",
|
||||
"back shot grassy field",
|
||||
"full body relaxed frame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "walking along path",
|
||||
"action": "hands in pockets",
|
||||
"camera": [
|
||||
"back shot autumn walkway",
|
||||
"side shot casual stride",
|
||||
"face zoom in subtle grin",
|
||||
"overhead tree shadows",
|
||||
"full body natural stance"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "kneeling by the bench",
|
||||
"action": "screaming again with wide-open mouth",
|
||||
"camera": [
|
||||
"face zoom in intensity",
|
||||
"side shot mouth open profile",
|
||||
"low angle roots and soil",
|
||||
"overhead leaf carpet",
|
||||
"zoom in to part of body—throat close"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 10,
|
||||
"time": "Afternoon",
|
||||
"location": "Trendy café corner with plants and soft lamps",
|
||||
"outfit": "gothic casual: black skirt, lace top, choker",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "sitting at table",
|
||||
"action": "smiling softly while stirring drink",
|
||||
"camera": [
|
||||
"full body cozy table shot",
|
||||
"side shot spoon and cup",
|
||||
"face zoom in gentle eyes",
|
||||
"overhead latte foam swirl",
|
||||
"back shot plant backdrop"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "standing beside the table",
|
||||
"action": "shouting with wide-open mouth",
|
||||
"camera": [
|
||||
"face zoom in bright lamps",
|
||||
"side shot mouth open",
|
||||
"low angle lamp glow",
|
||||
"overhead table layout",
|
||||
"zoom in to part of body—tensed throat"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "writing in notebook",
|
||||
"action": "biting lip thoughtfully",
|
||||
"camera": [
|
||||
"overhead page and pen",
|
||||
"face zoom in focused eyes",
|
||||
"side shot lace sleeve",
|
||||
"back shot framed by chairs",
|
||||
"full body seated posture"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "sipping from cup",
|
||||
"action": "looking away shyly",
|
||||
"camera": [
|
||||
"face zoom in eyes over rim",
|
||||
"side shot cup touch",
|
||||
"overhead saucer on table",
|
||||
"back shot warm ambience",
|
||||
"low angle lamp string bulbs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "kneeling on chair",
|
||||
"action": "screaming again with wide-open mouth",
|
||||
"camera": [
|
||||
"face zoom in scream",
|
||||
"side shot mouth open",
|
||||
"low angle chair legs",
|
||||
"overhead tabletop pattern",
|
||||
"zoom in to part of body—throat close"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 11,
|
||||
"time": "Evening",
|
||||
"location": "City rooftop with skyline lights",
|
||||
"outfit": "casual gothic: leather jacket, mini skirt, torn tights",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "standing on edge",
|
||||
"action": "arms open to the wind",
|
||||
"camera": [
|
||||
"full body skyline silhouette",
|
||||
"back shot city bokeh",
|
||||
"side shot hair lifted",
|
||||
"overhead roof lines",
|
||||
"face zoom in calm power"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "kneeling near the parapet",
|
||||
"action": "shouting with wide-open mouth",
|
||||
"camera": [
|
||||
"face zoom in stormy scream",
|
||||
"side shot mouth open",
|
||||
"low angle city glow",
|
||||
"overhead concrete texture",
|
||||
"zoom in to part of body—clenched fists"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "sitting on ledge",
|
||||
"action": "looking down at traffic",
|
||||
"camera": [
|
||||
"back shot roads below",
|
||||
"side shot dangling feet",
|
||||
"face zoom in reflective",
|
||||
"overhead ledge line",
|
||||
"full body relaxed sit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "walking across roof",
|
||||
"action": "wind blowing hair and jacket",
|
||||
"camera": [
|
||||
"full body wide stride",
|
||||
"side shot jacket flutter",
|
||||
"back shot trailing steps",
|
||||
"overhead roof grid",
|
||||
"face zoom in focused"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "small jump on roof seam",
|
||||
"action": "screaming again with wide-open mouth",
|
||||
"camera": [
|
||||
"face zoom in scream",
|
||||
"side shot mouth open",
|
||||
"overhead mid-air frame",
|
||||
"low angle skyline towers",
|
||||
"zoom in to part of body—throat close"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sceneId": 12,
|
||||
"time": "Night",
|
||||
"location": "Underground subway platform with graffiti",
|
||||
"outfit": "gothic streetwear: ripped jeans, black hoodie, silver chains",
|
||||
"cuts": [
|
||||
{
|
||||
"cutId": 1,
|
||||
"pose": "standing on platform",
|
||||
"action": "hands in pockets, head tilted",
|
||||
"camera": [
|
||||
"full body trackside shot",
|
||||
"side shot hoodie folds",
|
||||
"face zoom in under tube light",
|
||||
"overhead platform tiles",
|
||||
"back shot tunnel perspective"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 2,
|
||||
"pose": "leaning toward the tunnel",
|
||||
"action": "shouting with wide-open mouth",
|
||||
"camera": [
|
||||
"face zoom in echoing mouth",
|
||||
"side shot mouth open",
|
||||
"low angle rail shine",
|
||||
"overhead yellow line",
|
||||
"zoom in to part of body—tensed throat"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 3,
|
||||
"pose": "sitting on bench",
|
||||
"action": "head tilted down over chains",
|
||||
"camera": [
|
||||
"back shot empty platform",
|
||||
"side shot chain glint",
|
||||
"face zoom in shadowed eyes",
|
||||
"overhead bench geometry",
|
||||
"full body moody sit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 4,
|
||||
"pose": "walking along platform edge",
|
||||
"action": "swinging arms slowly",
|
||||
"camera": [
|
||||
"full body long leading lines",
|
||||
"side shot sneaker scuff",
|
||||
"face zoom in composed",
|
||||
"overhead fluorescent bands",
|
||||
"back shot receding tunnel"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cutId": 5,
|
||||
"pose": "arched back near the pillar",
|
||||
"action": "screaming again with wide-open mouth",
|
||||
"camera": [
|
||||
"face zoom in scream",
|
||||
"side shot mouth open",
|
||||
"low angle pillar grit",
|
||||
"overhead tiled grid",
|
||||
"zoom in to part of body—throat close"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
src/musicspot_generator/fire in the night/song.mp3
Normal file
BIN
src/musicspot_generator/fire in the night/song.mp3
Normal file
Binary file not shown.
BIN
src/musicspot_generator/girly girl/face.png
Normal file
BIN
src/musicspot_generator/girly girl/face.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
272
src/musicspot_generator/girly girl/scenes.json
Normal file
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
BIN
src/musicspot_generator/girly girl/song.mp3
Normal file
Binary file not shown.
240
src/musicspot_generator/images.ts
Normal file
240
src/musicspot_generator/images.ts
Normal file
@ -0,0 +1,240 @@
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
import { logger } from '../lib/logger';
|
||||
import { callOpenAI } from '../lib/openai';
|
||||
import { generateImage as generateFaceImage } from '../lib/image-generator-face';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
type Size = { width: number; height: number };
|
||||
|
||||
interface MusicSpotCharacter {
|
||||
bodyType: string;
|
||||
hairStyle: string;
|
||||
}
|
||||
|
||||
interface MusicSpotCut {
|
||||
cutId: number;
|
||||
pose: string;
|
||||
action: string;
|
||||
camera?: string[]; // list of camera variants per cut
|
||||
}
|
||||
|
||||
interface MusicSpotScene {
|
||||
sceneId: number;
|
||||
time: string;
|
||||
location: string;
|
||||
outfit: string;
|
||||
cuts: MusicSpotCut[];
|
||||
}
|
||||
|
||||
interface MusicSpotConfig {
|
||||
character: MusicSpotCharacter;
|
||||
scenes: MusicSpotScene[];
|
||||
}
|
||||
|
||||
interface Server {
|
||||
baseUrl?: string;
|
||||
outputDir?: string;
|
||||
inputDir?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SIZE: Size = { width: 720, height: 1280 };
|
||||
const FOLDER = process.argv[2] || process.env.MUSICSPOT_FOLDER || 'oputstise';
|
||||
const FOLDER_SAFE = FOLDER.replace(/[/\\?%*:|"<>]/g, '_');
|
||||
const FACE_SRC = path.resolve(`src/musicspot_generator/${FOLDER}/face.png`);
|
||||
const GENERATED_DIR = path.resolve('generated');
|
||||
|
||||
function loadServers(): Server[] {
|
||||
const servers: Server[] = [
|
||||
{
|
||||
name: 'SERVER1',
|
||||
baseUrl: process.env.SERVER1_COMFY_BASE_URL,
|
||||
outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR,
|
||||
},
|
||||
/*
|
||||
{
|
||||
name: 'SERVER2',
|
||||
baseUrl: process.env.SERVER2_COMFY_BASE_URL,
|
||||
outputDir: process.env.SERVER2_COMFY_OUTPUT_DIR,
|
||||
}, */
|
||||
]
|
||||
.filter((s) => !!s.baseUrl && !!s.outputDir)
|
||||
.map((s) => ({
|
||||
...s,
|
||||
inputDir: s.outputDir!.replace(/output/i, 'input'),
|
||||
}));
|
||||
|
||||
if (servers.length === 0) {
|
||||
logger.warn('No servers configured. Please set SERVER{N}_COMFY_BASE_URL and SERVER{N}_COMFY_OUTPUT_DIR in .env');
|
||||
} else {
|
||||
for (const s of servers) {
|
||||
logger.info(`Configured ${s.name}: baseUrl=${s.baseUrl}, outputDir=${s.outputDir}, inputDir=${s.inputDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
return servers;
|
||||
}
|
||||
|
||||
async function ensureDirs() {
|
||||
await fs.mkdir(GENERATED_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
async function copyFaceToServers(servers: Server[]): Promise<string> {
|
||||
const faceFileName = 'face.png';
|
||||
|
||||
// Validate face source
|
||||
try {
|
||||
await fs.access(FACE_SRC);
|
||||
} catch {
|
||||
throw new Error(`Face image not found at ${FACE_SRC}`);
|
||||
}
|
||||
|
||||
for (const srv of servers) {
|
||||
if (!srv.inputDir) continue;
|
||||
const dest = path.join(srv.inputDir, faceFileName);
|
||||
try {
|
||||
await fs.mkdir(srv.inputDir, { recursive: true });
|
||||
await fs.copyFile(FACE_SRC, dest);
|
||||
logger.info(`Copied face image to ${srv.name} input: ${dest}`);
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to copy face image to ${srv.name}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
return faceFileName; // Comfy expects file name present in input dir
|
||||
}
|
||||
|
||||
function buildImagePromptRequest(
|
||||
character: MusicSpotCharacter,
|
||||
scene: MusicSpotScene,
|
||||
cut: MusicSpotCut,
|
||||
cameraIntent: string
|
||||
): string {
|
||||
return `
|
||||
Return exactly one JSON object, nothing else: { "imagePrompt": "Cinematic realistic photo, (camera framing),(character),(pose),(time),(location),(outfit),(action),(lighting)" }.
|
||||
|
||||
Write "imagePrompt" in around 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
|
||||
|
||||
Only respond with JSON.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
async function getImagePromptFromOpenAI(req: string): Promise<string> {
|
||||
const res = await callOpenAI(req);
|
||||
const prompt = res?.imagePrompt || res?.image_prompt || res?.prompt;
|
||||
if (!prompt || typeof prompt !== 'string') {
|
||||
throw new Error('OpenAI failed to return imagePrompt JSON.');
|
||||
}
|
||||
return prompt.trim();
|
||||
}
|
||||
|
||||
function pickServer(servers: Server[], idx: number): Server {
|
||||
if (servers.length === 0) {
|
||||
throw new Error('No servers configured.');
|
||||
}
|
||||
return servers[idx % servers.length];
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await ensureDirs();
|
||||
|
||||
// Load scenes.json
|
||||
const configRaw = await fs.readFile(path.resolve(`src/musicspot_generator/${FOLDER}/scenes.json`), 'utf-8');
|
||||
const cfg: MusicSpotConfig = JSON.parse(configRaw);
|
||||
|
||||
const servers = loadServers();
|
||||
if (servers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure face.png in each server's input
|
||||
const faceFileName = await copyFaceToServers(servers);
|
||||
|
||||
// Generate images only (no video here). Intended to be run first.
|
||||
let imageTaskIndex = 0;
|
||||
|
||||
for (const scene of cfg.scenes) {
|
||||
logger.info(`=== Scene ${scene.sceneId}: Image generation start ===`);
|
||||
for (const cut of scene.cuts) {
|
||||
const cameraVariants =
|
||||
Array.isArray(cut.camera) && cut.camera.length > 0
|
||||
? cut.camera
|
||||
: ['eye-level medium shot', 'slight left 30°', 'slight right 30°', 'slight high angle', 'slight low angle'];
|
||||
|
||||
for (let camIdx = 0; camIdx < cameraVariants.length; camIdx++) {
|
||||
const cameraIntent = cameraVariants[camIdx];
|
||||
const variantIndex = camIdx + 1;
|
||||
const imgFileName = `${FOLDER_SAFE}_musicspot_s${scene.sceneId}_c${cut.cutId}_v${variantIndex}.png`;
|
||||
const outputPath = path.join(GENERATED_DIR, imgFileName);
|
||||
|
||||
// Skip generation if target file already exists
|
||||
try {
|
||||
await fs.access(outputPath);
|
||||
logger.info(`Skipping generation, file already exists: ${outputPath}`);
|
||||
continue;
|
||||
} catch {
|
||||
// File does not exist; proceed with generation
|
||||
}
|
||||
|
||||
// 1) Generate image prompt for this camera
|
||||
logger.info(`Scene ${scene.sceneId} - Cut ${cut.cutId} - Cam${variantIndex}: generating image prompt...`);
|
||||
const imgPromptReq = buildImagePromptRequest(cfg.character, scene, cut, cameraIntent);
|
||||
let imagePrompt: string;
|
||||
try {
|
||||
imagePrompt = await getImagePromptFromOpenAI(imgPromptReq);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`OpenAI image prompt failed for scene ${scene.sceneId} cut ${cut.cutId} cam ${variantIndex}: ${err}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2) Generate one image using face conditioning for this specific camera
|
||||
const serverForImage = pickServer(servers, imageTaskIndex++);
|
||||
logger.info(`Generating image (${imgFileName}) on ${serverForImage.name}...`);
|
||||
|
||||
try {
|
||||
// Use only the face file name for the workflow image input (Comfy expects it in its input dir)
|
||||
const finalImagePath = await generateFaceImage(
|
||||
`Realistic photo, ultra detailed, high contrast, ${imagePrompt}`,
|
||||
faceFileName,
|
||||
imgFileName,
|
||||
serverForImage.baseUrl!,
|
||||
serverForImage.outputDir!,
|
||||
DEFAULT_SIZE
|
||||
);
|
||||
logger.info(`Image generated: ${finalImagePath}`);
|
||||
} catch (err) {
|
||||
logger.error(`Image generation failed (${imgFileName}) on ${serverForImage.name}: ${err}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.info(`=== Scene ${scene.sceneId}: Image generation complete ===`);
|
||||
}
|
||||
|
||||
logger.info('Image generation for all scenes completed.');
|
||||
} catch (err) {
|
||||
logger.error('Fatal error in music spot image generator:', err);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
logger.error('Unhandled error:', err);
|
||||
});
|
||||
366
src/musicspot_generator/index.ts
Normal file
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
BIN
src/musicspot_generator/oputstise/face.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
827
src/musicspot_generator/oputstise/scenes.json
Normal file
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
BIN
src/musicspot_generator/oputstise/song.mp3
Normal file
Binary file not shown.
272
src/musicspot_generator/scenes.json
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
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 || 'oputstise';
|
||||
const FOLDER_SAFE = FOLDER.replace(/[/\\?%*:|"<>]/g, '_');
|
||||
const GENERATED_DIR = path.resolve('generated');
|
||||
|
||||
function loadServers(): Server[] {
|
||||
const servers: Server[] = [
|
||||
{
|
||||
name: 'SERVER1',
|
||||
baseUrl: process.env.SERVER1_COMFY_BASE_URL,
|
||||
outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR,
|
||||
},
|
||||
/*
|
||||
{
|
||||
name: 'SERVER2',
|
||||
baseUrl: process.env.SERVER2_COMFY_BASE_URL,
|
||||
outputDir: process.env.SERVER2_COMFY_OUTPUT_DIR,
|
||||
},*/
|
||||
]
|
||||
.filter((s) => !!s.baseUrl && !!s.outputDir)
|
||||
.map((s) => ({
|
||||
...s,
|
||||
inputDir: s.outputDir!.replace(/output/i, 'input'),
|
||||
}));
|
||||
|
||||
if (servers.length === 0) {
|
||||
logger.warn('No servers configured. Please set SERVER{N}_COMFY_BASE_URL and SERVER{N}_COMFY_OUTPUT_DIR in .env');
|
||||
} else {
|
||||
for (const s of servers) {
|
||||
logger.info(`Configured ${s.name}: baseUrl=${s.baseUrl}, outputDir=${s.outputDir}, inputDir=${s.inputDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
return servers;
|
||||
}
|
||||
|
||||
async function ensureDirs() {
|
||||
await fs.mkdir(GENERATED_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function buildVideoPromptRequest(
|
||||
character: MusicSpotCharacter,
|
||||
scene: MusicSpotScene,
|
||||
cut: MusicSpotCut,
|
||||
cameraIntent: string
|
||||
): string {
|
||||
return `
|
||||
Return exactly one JSON object and nothing else: { "videoPrompt": "..." }.
|
||||
|
||||
Write "videoPrompt" in 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,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
await sleep(10000); // wait a bit for file system to settle
|
||||
logger.info(`Video generated: ${videoPath}`);
|
||||
} catch (err) {
|
||||
logger.error(`Video generation failed (${videoFileName}) on ${serverForVideo.name}: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.info(`=== Scene ${scene.sceneId}: Video generation complete ===`);
|
||||
}
|
||||
|
||||
logger.info('Video generation for all scenes completed.');
|
||||
} catch (err) {
|
||||
logger.error('Fatal error in music spot video generator:', err);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
logger.error('Unhandled error:', err);
|
||||
});
|
||||
Reference in New Issue
Block a user