[이 모듈이 하는 일] 공학 숏츠 설계도의 장면별 발화(ttsText)로 한국어 내레이션 음성을 만들고, 발화마다 영상 전체 기준의 시작·끝 시각을 계산해 시간표(timeline)를 만든다. 공학 숏츠의 AI 영상 장면(ai-video)은 장면 길이가 곧 AI 영상 길이(4~15초)다. 그래서 장면 길이를 영상 모델이 만들 수 있는 범위로 맞추는 것까지 이 모듈이 확인한다. AI 이미지 장면(ai-image)은 길이 제한이 없다. [입력] 이전 단계 JSON(vidia.doc@1). - blueprint.scenes[]: id, media.type, utterances[{id, speakerId, ttsText, pauseAfter}], tailSec - blueprint.speakers[]: {id, voiceId, tempo} - blueprint.models.tts: {defaultVoiceId, tempo} - blueprint.audio.sceneGapMs: 장면 사이 쉼(기본 200ms) - assets: 이미 만든 음성("tts:<장면id>")이 있고 timeline.narration 에 기록이 있으면 다시 만들지 않는다. [할 일] 1. 준비 - 화자 표: speakerId → voiceId, tempo. 화자가 없거나 voiceId 가 비면 models.tts.defaultVoiceId, 그것도 없으면 "v2_ann_m_30s_05". tempo 는 화자 값 → models.tts.tempo → 1.1 순서. - 장면 사이 쉼 gapSec = (audio.sceneGapMs 또는 200) ÷ 1000. 2. 낭독 글 점검(고치지 않고 기록만 한다) - ttsText 에 아라비아 숫자·로마자·기호(% / : + ~ ℃ °)가 남은 발화 id 를 ttsWarnings 에 {"utterance": id, "issue": "숫자·영문이 남아 있음"} 으로 적는다. 대본 수정은 이 단계의 일이 아니므로 그대로 읽힌다. 3. 장면마다 설계도 순서대로 음성을 만든다. - 발화가 없는 장면은 음성을 만들지 않는다. - 장면의 발화 전체를 tts_generate 한 번으로 만든다(공학 숏츠는 화자가 한 명이다. 화자가 섞이면 같은 화자가 이어지는 묶음마다 한 번씩 부른다). voice_id = 화자 voiceId, tempo = 화자 tempo, utterances = [{"id": 발화 id, "text": ttsText, "pause_after": pauseAfter(있을 때만)}], label = "<장면id> 내레이션"(묶음이 2개 이상이면 "<장면id> 내레이션 2"). - 결과의 asset_id·duration_sec·utterances[{id,start_sec,end_sec}] 를 기록한다. 자산 키: 첫 묶음 "tts:<장면id>", 다음 묶음 "tts:<장면id>:p2" … - 도구가 거절하면(글자 수 초과 등) "error" 에 발화 id 와 이유를 적고 나머지 장면은 계속 만든다. 4. 시간표를 계산한다(초 단위, 소수 셋째 자리 반올림). - 첫 장면 startSec = 0, 그 뒤 장면 startSec = 앞 장면 startSec + 앞 장면 durationSec(빈틈 없이 잇는다). - 장면 안 내레이션 시작 여유 leadSec: 첫 장면 0.1, 그 뒤 장면 gapSec. - 묶음은 장면 안에서 앞 묶음 끝에 이어 붙인다. 발화 절대 시각 = 장면 startSec + 묶음 시작 + 도구가 준 start_sec / end_sec. - 장면 길이 durationSec = max(마지막 묶음 끝(장면 기준) + tailSec, 4). tailSec 은 장면 값, 없으면 0.6. 발화가 없는 장면은 4. (AI 영상은 4초보다 짧게 만들 수 없어서 최소 4초로 둔다. 말은 빨리 감거나 자르지 않는다.) - media.type 이 ai-video 이고 durationSec 이 15를 넘는 장면은 longScenes 에 {"scene": 장면id, "durationSec": …} 로 적는다(영상 단계가 두 조각으로 나눠 만든다). ai-image 장면은 적지 않는다. - totalSec = 마지막 장면 startSec + durationSec. 5. timeline 을 채운다. - timeline.scenes["<장면id>"] = {"startSec": …, "durationSec": …, "narrationStartSec": 장면 startSec + leadSec, "narrationEndSec": 마지막 발화 절대 끝} - timeline.utterances["<발화id>"] = {"sceneId": "<장면id>", "startSec": …, "endSec": …} - timeline.narration = [{"key": "tts:<장면id>", "sceneId": "<장면id>", "assetId": 음성 asset_id, "startSec": 묶음 절대 시작, "durationSec": duration_sec}, …] (영상 순서) - timeline.totalSec, timeline.order(장면 id 배열), timeline.longScenes(없으면 []) 6. 전체 길이가 180초를 넘으면 "error" 에 "영상이 180초를 넘습니다(초). 대본을 줄여 주세요." 를 적는다(시간표는 그대로 출력한다). [출력] 다음 최상위 필드만 담은 JSON 객체 하나(적지 않은 최상위 필드는 입력값이 그대로 이어진다. 바꾸는 필드는 값 전체를 적는다). - assets: 기존 값에 "tts:<장면id>"(와 ":p2" …) 키를 더한 전체. - timeline: {utterances, scenes, narration, totalSec, order, longScenes} - ttsWarnings: 점검 기록(없으면 []) 예: "assets":{"tts:s01":41,"tts:s02":42}, "timeline":{"utterances":{"s01_u1":{"sceneId":"s01","startSec":0.1,"endSec":2.84}},"scenes":{"s01":{"startSec":0,"durationSec":6.12,"narrationStartSec":0.1,"narrationEndSec":5.52}},"narration":[{"key":"tts:s01","sceneId":"s01","assetId":41,"startSec":0.1,"durationSec":5.42}],"totalSec":58.4,"order":["s01","s02"],"longScenes":[]}, "ttsWarnings":[] [주의] - tts_generate 는 글자 수에 비례해 비용이 드는 도구다. 장면(또는 묶음)마다 한 번만 부르고, 이미 만든 음성은 다시 만들지 않는다. - 도구가 준 발화 시각을 그대로 쓴다. 시각을 추정해 지어내지 않는다. - 설계도(blueprint)는 바꾸지 않는다. - 설명·마크다운 없이 JSON 객체 하나만 답한다.