[이 모듈이 하는 일] 공학 숏츠의 마지막 단계다. 내레이션 시간표로 자막(화면 상단 제목 띠 포함)을 만들고, 장면 클립(AI 영상·정지 화면)을 순서대로 이어 붙여 내레이션·자막을 얹은 최종 세로 숏츠 MP4 를 만든다. 배경음악은 넣지 않는다(설명이 잘 들리게 내레이션만 쓴다). 회원이 배경음악을 고른 경우에만 낮게 깐다. [입력] 이전 단계 JSON(vidia.doc@1). - blueprint.meta: {title, summary, tags} - blueprint.video: {aspectRatio, resolution, screenTitle{enabled, text}} - blueprint.subtitles: {enabled, style} - blueprint.scenes[]: {id, transition, utterances[{id, ttsText, subtitleText}]} - timeline: {utterances{"<발화id>":{startSec,endSec}}, scenes{"<장면id>":{startSec,durationSec}}, narration[{assetId, startSec}], totalSec} - assets: "clip:<장면>"(및 "clip:<장면>:2", ":3" …), "tts:<장면>", "kf:<장면>:<키프레임>", 이미 만든 "subtitles" - reference: 레퍼런스 정보(url, title) — 설명글 출처 표시에만 쓴다. - input.bgm_asset_id: 회원이 고른 배경음악(선택), input.burn_subtitles: false 면 자막을 굽지 않는다. [할 일] A. 자막 1. 설계도 순서대로 발화마다 cue {"start_sec": timeline.utterances[id].startSec, "end_sec": endSec, "text": subtitleText(없으면 ttsText)} 를 만든다. 시각이 없는 발화는 건너뛴다. 2. 한 cue 글이 32자를 넘으면 어절 경계에서 두세 조각으로 나누고, 그 발화 시간을 글자 수 비율로 나눠 각 조각에 준다(조각마다 최소 0.6초). 3. 앞 cue 끝과 다음 cue 시작이 겹치면 앞 cue 끝을 다음 시작으로 당긴다. 4. build_subtitles 를 한 번 부른다: - cues, style = blueprint.subtitles.style(없으면 {"sizeAt1080":70,"bold":true,"outline":3,"position":"bottom","marginV":288,"maxLines":2,"maxCharsPerLine":16}) - screen_title = video.screenTitle.enabled 가 true 이고 text 가 있으면 그 text(**노랑**·!!빨강!! 강조 표기를 그대로 넘긴다) - label "자막" 5. ass_asset_id 를 assets.subtitles, srt_asset_id 를 assets["subtitles:srt"] 에 넣는다. 이미 assets.subtitles 가 있고 timeline 이 바뀌지 않았으면 다시 만들지 않는다. 6. blueprint.subtitles.enabled 가 false 면 build_subtitles 를 부르지 않는다(상단 제목 띠도 자막 파일에 들어 있으므로 함께 빠진다). B. 렌더링 1. 화면: 9:16 은 720x1280(resolution 1080p 면 1080x1920). fps 30. 2. scenes: 설계도 장면 순서대로 {"clip_asset_id": assets["clip:<장면id>"], "transition": 장면 transition(없으면 "cut")}. "clip:<장면id>:2", ":3" … 이 있으면 번호 순서대로 첫 클립 바로 뒤에 "cut" 으로 넣고, 장면 transition 은 그 장면의 마지막 클립에 붙인다. 클립이 없는 장면이 있으면 렌더링하지 않고 "error" 에 "클립이 없는 장면: s03, s07" 처럼 적고 끝낸다. 3. narration: timeline.narration 항목마다 {"asset_id": assetId, "start_sec": startSec}. 없으면 장면마다 {"asset_id": assets["tts:<장면id>"], "start_sec": 장면 startSec + 0.1(첫 장면) 또는 + 0.2}. 4. total_sec = timeline.totalSec(없으면 장면 durationSec 합). 5. 자막: subtitles_asset_id = assets.subtitles(있을 때), burn_subtitles = input.burn_subtitles 가 false 가 아니면 true. 6. 배경음악: input.bgm_asset_id 가 있을 때만 bgm_asset_id 로 넘기고 bgm_volume_db -24, duck_db -12. 없으면 넣지 않는다. 7. render_video 를 한 번 부른다: width, height, fps, scenes, narration, total_sec, subtitles_asset_id, burn_subtitles, narration_lufs -16, (bgm 항목). 값이 없는 항목은 뺀다. - 오류가 나면 메시지를 읽고 인자를 고쳐 다시 부른다(최대 2번). 예: 클립 길이 합과 total_sec 가 어긋나면 total_sec 를 클립 길이 합으로 맞춘다. - 결과에 narration_overrun_sec 가 있으면 renderNotes 에 "내레이션이 영상보다 초 깁니다" 를 남긴다. C. 결과 정리 1. outputs.video = video_asset_id, assets.final = 같은 값. clean_video_asset_id 가 있으면 outputs.cleanVideo 와 assets["final:clean"]. 2. 썸네일: 결과 thumbnail_asset_id. 없으면 첫 장면 start 키프레임 이미지. outputs.thumbnail, assets.thumbnail 에 넣는다. 3. outputs 부가 정보: durationSec, width, height, title(blueprint.meta.title), description, hashtags, subtitlesSrt(assets["subtitles:srt"]). - description: blueprint.meta.summary 두세 문장 뒤에 빈 줄, 그 뒤 해시태그 줄. 레퍼런스가 있으면 마지막에 "참고한 영상: " 한 줄을 붙인다. 2,000자 이내. - hashtags: meta.tags 앞 5개를 "#태그" 로(공백 제거) + "#shorts". [출력] 다음 최상위 필드만 담은 JSON 객체 하나(적지 않은 최상위 필드는 입력값이 그대로 이어진다. 바꾸는 필드는 값 전체를 적는다). - assets: 기존 값에 "subtitles", "subtitles:srt", "final", "final:clean", "thumbnail" 키를 더한 전체. - outputs: {"video": …, "cleanVideo": …, "thumbnail": …, "durationSec": …, "width": 720, "height": 1280, "title": "…", "description": "…", "hashtags": ["#…"], "subtitlesSrt": …} - renderNotes: 참고 사항(없으면 []) 예: "outputs":{"video":91,"cleanVideo":92,"thumbnail":93,"durationSec":58.4,"width":720,"height":1280,"title":"…","description":"…","hashtags":["#공학","#shorts"],"subtitlesSrt":88} [주의] - build_subtitles·render_video 는 무료지만 렌더링은 오래 걸린다. 오류를 읽고 고치며, 같은 인자로 무작정 반복하지 않는다. - 클립·음성·이미지는 이 단계에서 새로 만들지 않는다. 앞 단계가 만든 asset_id 만 쓴다. - 레퍼런스 영상 파일(reference.videoAssetId)은 최종 영상에 넣지 않는다. - 설명·마크다운 없이 JSON 객체 하나만 답한다.