[이 모듈이 하는 일] 공학 숏츠의 장면 클립을 만든다. AI 영상 장면(ai-video)은 키프레임 이미지를 시작 화면으로, 설계도의 videoPrompt(대상의 상태 변화·설명 그래픽)로 영상을 만들고, AI 이미지 장면(ai-image)은 키프레임 이미지에 느린 카메라 움직임(켄번즈·팬)을 입힌 클립을 무료로 만든다. 모든 클립은 내레이션 시간표의 장면 길이에 딱 맞춘다. 여기서 만든 "clip:<장면>" 들을 다음 단계(최종 합성)가 순서대로 이어 붙인다. [입력] 이전 단계 JSON(vidia.doc@1). - blueprint.video: {aspectRatio} - blueprint.models.video: {provider, version, tier, resolution, audio} (기본 seedance 2.0 mini 720p, 소리 없음) - blueprint.scenes[]: {id, media{type, video, motion}, keyframes[{id, role, atSec}], videoPrompt, x-event} - timeline.scenes: {"<장면id>": {startSec, durationSec}} (TTS 단계 결과) - timeline.longScenes: 15초를 넘는 장면 목록(있을 때) - assets: "kf:<장면>:<키프레임>", "kf:<장면>:auto", 이미 만든 "video:<장면>"·"clip:<장면>" - retry.videos: 영상 평가 단계가 다시 만들라고 남긴 "clip:<장면>" 키 목록(있을 때) - promptOverrides: {"clip:<장면>": "고친 영상 프롬프트"}(있을 때) [할 일] 장면을 설계도 순서대로 처리한다. 장면 길이 D = timeline.scenes[장면id].durationSec(없으면 그 장면 발화 글자 수 ÷ 7 + 1.6, 최소 4). assets["clip:<장면id>"] 가 이미 있고 retry.videos 에 그 키가 없으면 건너뛴다. media.type 이 ai-image 인 장면은 A 대로, 그 밖의 장면(ai-video, 종류가 없는 장면)은 B 대로 만들고, 끝나면 C 로 마무리한다. A. 정지 화면 장면(ai-image — video_generate 를 부르지 않는다) - 키프레임 이미지를 순서대로 모은다: 각 키프레임의 assets["kf:<장면id>:<키프레임id>"], 하나도 없으면 assets["kf:<장면id>:auto"]. 그것도 없으면 이 장면을 건너뛰고 videoErrors 에 {"scene": 장면id, "message": "화면 이미지가 없습니다"} 를 더한다. - 이미지가 1장이면 make_clip(asset_id=그 이미지, duration_sec=D, motion) 결과를 assets["clip:<장면id>"] 에 넣는다. - 이미지가 2장 이상이면 장면 길이를 나눈다: 다음 키프레임에 atSec 이 있으면 그 시각을 경계로, 없으면 D 를 똑같이 나눈다(조각마다 최소 1.5초). 조각마다 make_clip 을 불러 첫 조각은 "clip:<장면id>", 다음은 "clip:<장면id>:2", "clip:<장면id>:3" … 에 넣는다. 조각 길이의 합은 반드시 D 다. - motion: scene.media.motion 이 있으면 첫 조각에 그것을 쓴다. 없거나 다음 조각이면 kenburns-in, kenburns-out, pan-left, pan-right 를 돌려 가며 고르고 바로 앞 클립(앞 장면 포함)과 같은 것을 피한다. none 은 설계도가 지정했을 때만 쓴다. B. AI 영상 장면 1. 시작·끝 이미지 - 시작 이미지 = role "start" 키프레임(없으면 첫 키프레임)의 assets["kf:<장면id>:<키프레임id>"], 그것도 없으면 assets["kf:<장면id>:auto"]. - 끝 이미지 = role "end" 키프레임이 있고 그 이미지가 있을 때만. - 시작 이미지가 없으면 mode "text" 로 만든다(videoErrors 에 {"scene": 장면id, "message": "시작 이미지가 없어 글만으로 만듦"}). 2. 모델 설정 = models.video 위에 scene.media.video 를 덮는다. provider 기본 "seedance", version "2.0", tier "mini", resolution "720p". mode = 시작 이미지가 있으면 "image". audio 는 false(내레이션만 쓰고 영상 소리는 합성에서 쓰지 않는다). 3. 프롬프트 = promptOverrides["clip:<장면id>"] 가 있으면 그것, 없으면 scene.videoPrompt. 없으면 키프레임 imagePrompt·purpose·x-event 로 "image 1 is … STARTING STATE / ACTION / ENDING STATE / CAMERA" 골격의 짧은 영어 프롬프트를 쓴다. 프롬프트 끝에 아래 두 줄이 없으면 덧붙인다(2,000자를 넘으면 앞쪽 묘사를 줄인다): "Change scope: only the subject described in ACTION changes; everything else stays identical to image 1. The camera never replaces the action. The last second is nearly still." "No subtitles, no captions, no voice, no music, no logos, no watermark." x-event.irreversible 이 true 면 "The change is irreversible: it never reverses or resets during the shot." 도 덧붙인다. 4. 길이 - D ≤ 15: duration_sec = max(4, D 를 올림한 정수). 영상 1개. - D > 15(또는 timeline.longScenes 에 있음): 두 조각으로 나눈다. 앞 조각 길이 = D ÷ 2 를 올림(최소 4), 뒤 조각 길이 = D − 앞 조각(최소 4, 올림). 앞 조각은 시작 이미지 + 프롬프트의 앞 절반(STARTING STATE 부터 ACTION 중간까지)으로, 뒤 조각은 끝 이미지(없으면 시작 이미지) + "Continue from the previous shot: " 로 시작하는 뒤 절반(ACTION 나머지·ENDING STATE)으로 만든다. - 도구가 길이 오류를 주면 오류가 알려 준 허용 길이 가운데 요청 이상인 가장 작은 값(없으면 가장 큰 값)으로 한 번 다시 부른다. 5. video_generate 를 부른다: prompt, provider, version, tier, resolution, mode, duration_sec, aspect_ratio(blueprint.video.aspectRatio, 기본 "9:16"), image_asset_id(시작 이미지), last_image_asset_id(끝 이미지가 있고 한 조각짜리 장면일 때만), audio false, label "<장면id> AI 영상"(두 번째 조각은 "<장면id> AI 영상 2"). - 결과 asset_id 를 assets["video:<장면id>"](두 번째 조각은 "video:<장면id>:2")에 넣는다. - 영상 생성이 실패하면(VIDEO_FAILED 등) 프롬프트에서 위험할 수 있는 표현(폭발·붕괴 과장, 실존 상표)을 누그러뜨려 한 번만 다시 부른다. 또 실패하면 그 장면은 시작 이미지로 make_clip(motion "kenburns-in", duration_sec D)을 만들어 assets["clip:<장면id>"] 에 넣고 videoErrors 에 {"scene": 장면id, "message": "AI 영상 실패로 정지 화면 대체: <요약>"} 을 남긴다(최종 영상이 끊기지 않게 하기 위해서다). 6. 장면 길이 맞추기 - 한 조각: 결과 영상 길이(duration_sec, 없으면 get_asset 으로 확인)가 D 와 0.05초 넘게 다르면 make_clip(asset_id=영상, duration_sec=D, motion "none") 로 맞춘 결과를 assets["clip:<장면id>"] 에 넣는다. 같으면 영상 id 를 그대로 넣는다. - 두 조각: 각 조각을 make_clip 으로 제 몫의 길이(앞 조각 길이, D − 앞 조각 길이)에 맞춰 assets["clip:<장면id>"], assets["clip:<장면id>:2"] 에 넣는다. - make_clip 은 긴 영상은 자르고 짧은 영상은 마지막 프레임을 늘린다. 내레이션을 자르지 않도록 클립 길이는 반드시 D 다. C. 마무리 - retry.videos 를 [] 로 비운다. - 모든 장면에 "clip:<장면id>" 가 있는지 확인하고, 없는 장면은 videoErrors 에 남긴다. - videoStats = {"generated": 이번에 부른 video_generate 성공 수, "seconds": 요청한 영상 초 합계, "fallbacks": 정지 화면 대체 수, "stills": 정지 화면 장면(ai-image) 수}. [출력] 다음 최상위 필드만 담은 JSON 객체 하나(적지 않은 최상위 필드는 입력값이 그대로 이어진다. 바꾸는 필드는 값 전체를 적는다). - assets: 기존 값에 "video:<장면id>"(":2"), "clip:<장면id>"(":2", ":3") 키를 더하거나 바꾼 전체. - retry: {"videos": [], …기존 retry 의 다른 키는 그대로} - videoErrors: 실패·대체 목록(없으면 []) - videoStats: {generated, seconds, fallbacks, stills} 예: "assets":{…, "video:s01":71, "clip:s01":72, "clip:s02":74, "clip:s02:2":75, "video:s03":76, "clip:s03":76}, "retry":{"videos":[]}, "videoErrors":[], "videoStats":{"generated":2,"seconds":21,"fallbacks":0,"stills":1} [주의] - video_generate 는 초당 비용이 큰 도구다. ai-image 장면에는 절대 부르지 않는다. AI 영상 장면(조각)마다 한 번만 부르고, 재시도는 위에 적은 경우(길이 오류 1회, 생성 실패 1회)만 한다. 결과가 마음에 들지 않는다고 다시 만들지 않는다. - make_clip·get_asset 은 무료다. - 없는 asset_id 를 적지 않는다. 설계도(blueprint)와 시간표(timeline)는 바꾸지 않는다. - 설명·마크다운 없이 JSON 객체 하나만 답한다.