[이 모듈이 하는 일] 장면마다 최종 렌더링에 쓸 영상 클립 하나를 만든다. AI 영상 장면(ai-video)은 키프레임 이미지를 시작(과 끝) 화면으로 AI 영상을 만들고, 나머지 장면은 키프레임·실사진·스톡·내 자료로 움직이는 클립(켄번즈·길이 맞춤)을 만든다. [입력] 이전 단계 JSON(vidia.doc@1). - blueprint.video: {aspectRatio, resolution} - blueprint.models.video: {provider, version, tier, resolution, audio} (기본 seedance 2.0 mini 720p) - blueprint.scenes[]: {id, media{type, motion, video, stock, assetId}, keyframes[{id, role}], videoPrompt} - timeline.scenes: {"<장면id>": {startSec, durationSec}} (TTS 단계 결과. 없으면 설계도 durationSec, 그것도 없으면 발화 글자 수 ÷ 7 + 0.5초) - assets: "kf:<장면>:<키프레임>", "media:<장면>", 이미 만든 "video:<장면>"·"clip:<장면>" - retry.videos: 영상 평가 단계가 다시 만들라고 남긴 장면 키 목록(있을 때) - promptOverrides: {"clip:<장면>": "고친 영상 프롬프트"} (있을 때) - input.video_provider / input.video_tier: 회원이 고른 영상 모델(있으면 models.video 보다 먼저) [할 일] 장면을 설계도 순서대로 처리한다. 장면 길이 D = timeline.scenes[장면id].durationSec. assets["clip:<장면id>"] 가 이미 있고 retry.videos 에 그 키가 없으면 건너뛴다. A. ai-video 장면 1. 시작 이미지 = role 이 "start" 인 키프레임(없으면 첫 키프레임)의 assets["kf:…"]. 끝 이미지 = role 이 "end" 인 키프레임이 있으면 그 asset. 시작 이미지가 없으면 B 의 방법(키프레임 없는 경우)으로 대체하고 videoErrors 에 남긴다. 2. 모델 설정 = models.video 위에 scene.media.video 를 덮고, 그 위에 input.video_provider·input.video_tier 를 덮는다. mode 는 scene.media.video.mode, 없으면 "image". 3. duration_sec = D 를 올림한 정수(최소 3, 최대 30). 도구가 모델이 허용하는 길이로 올려 맞춘다. 그래도 길이 오류를 주면, 오류가 알려 준 허용 길이 가운데 D 이상인 가장 작은 값(없으면 가장 큰 값)으로 다시 부른다. 4. prompt = promptOverrides["clip:<장면id>"] 가 있으면 그것, 없으면 scene.videoPrompt. 끝에 "No text, no subtitles, no logos, no voice, no music." 가 없으면 덧붙인다. 단 models.video.audio 가 true 면(영상 모델이 소리·대사까지 만드는 설계도) "No text, no subtitles, no logos." 만 덧붙이고, 프롬프트에 적힌 SOUND 줄·대사는 지우지 않는다. videoPrompt 가 없으면 키프레임 imagePrompt 와 장면 purpose 로 "STARTING STATE / ACTION / ENDING STATE / CAMERA" 골격의 짧은 영어 프롬프트를 쓴다. 5. video_generate 를 부른다: prompt, provider, version, tier, resolution, mode, duration_sec, aspect_ratio(blueprint.video.aspectRatio), image_asset_id(시작 이미지), last_image_asset_id(끝 이미지가 있을 때만), audio(models.video.audio 가 true 일 때만 true, 기본 false). 6. 결과 영상 asset_id 를 assets["video:<장면id>"] 에 넣는다. 7. 결과 영상 길이(duration_sec)가 D 와 0.2초 넘게 다르면 make_clip(asset_id=그 영상, duration_sec=D) 로 길이를 맞춰 그 결과를 assets["clip:<장면id>"] 에 넣는다. 길이가 맞으면 영상 asset_id 를 그대로 assets["clip:<장면id>"] 에 넣는다. 결과에 길이가 없으면 get_asset 으로 확인한다. make_clip 은 짧은 영상의 마지막 프레임을 늘리고 긴 영상은 자른다. B. ai-image·photo·stock-video·upload 장면 1. 원본 자산을 고른다(위에서부터 처음 있는 것): - photo·stock-video·upload: assets["media:<장면id>"]. upload 장면은 scene.media.assetId 도 쓸 수 있다. - 그 장면의 키프레임 이미지: role "start" 키프레임(없으면 첫 키프레임)의 assets["kf:…"], 또는 assets["kf:<장면id>:auto"]. - 없으면 이 장면을 건너뛰고 videoErrors 에 {"scene": 장면id, "message": "화면 자산이 없습니다"} 를 더한다. 2. make_clip 을 부른다. - asset_id: 1번 자산 - duration_sec: D - motion: 원본이 이미지면 scene.media.motion(없으면 장면 순서에 따라 kenburns-in, kenburns-out, pan-left, pan-right 를 돌려 가며 — 바로 앞 장면과 같은 것을 피한다). 원본이 영상이면 "none". - start_sec: stock-video 이고 scene.media.stock.startSec 가 있으면 그 값(영상 원본일 때만). 3. ai-image 장면에 키프레임이 2장 이상이면 키프레임마다 클립을 만든다. 길이는 atSec 이 있으면 그 경계로, 없으면 D 를 똑같이 나눈다. 첫 클립은 "clip:<장면id>", 다음은 "clip:<장면id>:2", "clip:<장면id>:3" … 4. 결과 asset_id 를 assets["clip:<장면id>"] 에 넣는다. C. 마무리 - retry.videos 를 [] 로 비운다. - 모든 장면에 "clip:<장면id>" 가 있는지 확인하고, 없는 장면은 videoErrors 에 남긴다. [출력] 다음 최상위 필드만 담은 JSON 객체 하나(적지 않은 최상위 필드는 입력값이 그대로 이어지므로 다시 적지 않는다. 바꾸는 필드는 값 전체를 적는다.) - assets: "video:<장면id>"(AI 영상 원본), "clip:<장면id>"(장면 최종 클립, 필요하면 ":2" …)를 더하거나 바꾼다. - retry: {"videos": [], …기존 retry 의 다른 키는 그대로} - videoErrors: 실패 목록(없으면 []) 예: "assets":{…, "video:s01":71, "clip:s01":72, "clip:s02":73} [주의] - video_generate 는 초당 비용이 큰 도구다. ai-video 장면에만, 장면마다 한 번만 부른다(길이 오류로 인한 재시도 1회 제외). 결과가 마음에 들지 않는다고 다시 만들지 않는다(품질 판정은 평가 단계가 한다). - make_clip 은 무료다. - 없는 asset_id 를 적지 않는다. - 설계도(blueprint)와 시간표(timeline)는 바꾸지 않는다. - 설명·마크다운 없이 JSON 객체 하나만 답한다.