capcut-agent/레이어_삐짐_수리.md
hehihoho3@gmail.com bf1b387d6d chore: git 저장소 초기화 (기존 코드 스냅샷)
컷별 댓글 추천 작업을 태스크 단위로 되돌릴 수 있게 버전관리를 시작한다.
.gitignore 로 영상·캐시(.downloads 2.7G, .comments 72M, .media 28M)와
비밀키(.gemini_key)를 제외했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:36:05 +09:00

14 KiB

영상이 템플릿 밖으로 삐져나오는 문제 — 원인·조치 기록

작성 2026-07-27. 관련 코드: capcut_agent/draft.py, server/app.py, server/static/index.html. 배경 지식은 ARCHITECTURE.md §5(드래프트 빌더) / §10(하지 말 것)를 볼 것 — 여기선 중복하지 않는다.

1. 증상

캡컷에서 클립을 복사하거나 옮긴 뒤 확대하면, 그 클립만 흰 띠(frame)와 댓글 카드 (comment) 위로 올라가 템플릿 밖으로 삐져나온다. 다른 클립은 멀쩡하다.

2. 원인 (실측 증거)

탭별 코드 차이가 아니다. 세 탭 모두 같은 빌더(build_bg_template_draft)를 쓰고, 생성 직후 draft_content.json은 완전히 동일하다.

구간탭 드래프트 (사용자가 클립 옮김/복사함)
  bg  ri=[0]   main ri=[1, 4]   frame ri=[2]   comment ri=[3]
                       ↑ 옮기거나 복사한 클립만 4 = frame·comment 보다 위

붙여넣기 드래프트 (클립을 안 건드림)
  bg  ri=[0]   main ri=[1]      frame ri=[2]   comment ri=[3]   ← 정상

캡컷이 새로 생긴 비디오 세그먼트(복사·이동)에 렌더순서를 새로 찍는다. 붙여넣기 탭이 멀쩡해 보인 건 거기서 클립을 안 옮겼기 때문이고, 거기서도 옮기면 똑같이 깨진다.

재현 2회 모두 값이 4였다:

시점 조작 결과
1차 클립 2개를 타임라인 앞으로 이동 그 2개만 ri=4 (확대 189.8%)
2차 수리 후 클립 1개 복사 + 확대 그 1개만 ri=4 (확대 225.6%)

⚠ 2026-07-31 반례: 악뮤 수현의 하루_…_하이라이트3에서 복붙+확대(201.7%) 클립이 ri=3을 받았다(비디오 최대는 comment의 3 → max+1이면 4여야 함). 즉 "정확히 max+1" 규칙은 보편이 아니다. 확실한 결론은 "복붙/이동 클립의 ri가 frame(2) 위로 재부여된다" 뿐이며, 수리는 정상값으로 되돌리는 방식이라 3이든 4든 동일하게 고쳐진다.

왜 트랙 잠금으로는 못 막나

_lock_tracksbg·frame·comment·제목 트랙만 잠근다. main은 사용자가 편집해야 하므로 잠글 수 없고, 문제는 그 main 트랙 안에서 일어난다.

왜 생성 시점 값으로도 못 막나 (규칙이 max+1이라면)

텍스트 쪽 증거상 캡컷은 새 세그먼트에 같은 타입 대역 안에서 현재 최대 + 1 을 준다 (사용자가 추가한 텍스트가 14109 → 14110 → … 순차 증가). 비디오도 같은 규칙이면 frame을 아무리 높여도 새 클립이 그보다 +1을 받으므로 예방이 원리적으로 불가능하다. 텍스트(14000+)는 비디오와 대역이 달라 영향을 받지 않는다.

3. 적용한 것

(a) 레이어 수리 기능 — 사후 복구 (확실히 동작함)

capcut_agent/draft.py

이름 역할
CANON_RI 트랙별 정상 render_index — bg 0 / main 1 / frame 2 / comment 3
_count_bad_ri(json_path) 정상값과 다른 비디오 세그먼트 수 = 꼬임 개수
list_drafts(draft_root) 드래프트 목록(최근 수정순) + broken/bad 플래그
repair_layers(draft_dir) 비디오 세그먼트 render_index를 트랙별 정상값으로 되돌리고 _lock_tracks 재적용. 이름 없는(사용자 추가) 비디오 트랙은 4,5… 로 밀어 맨 위 유지

⚠ 2026-08-03: 아래 표는 루트 draft_content.json 다루던 시절 기록이다. 지금은 Timelines/* 사본까지 함께 고친다 — (a-3) 을 반드시 볼 것.

  • 백업은 <파일명>.repair.bak. draft_content.json.bak캡컷 자체 백업 파일명이라 쓰면 안 된다(처음에 이걸로 썼다가 캡컷 백업을 덮어썼음 — 같은 실수 반복 금지).
  • 고칠 게 없으면 백업도 안 만들고 잠금만 다시 채운다(멱등).

server/app.py

  • GET /drafts{"drafts":[{name, mtime, broken, bad}, …]} 최근 30개
  • POST /repair (form draft=<드래프트폴더명>) → {"ok", "fixed", "detail"} 경로는 os.path.basename으로 정규화해 드래프트 루트 밖으로 못 나가게 막음. _capcut_running() 이면 409로 거부(force=1 로만 강행). 실측: 11:04:32 수리 → 11:05:39 CapCut 저장으로 되돌아감. CapCut 은 파일을 다시 읽지 않고 메모리 상태로 덮어쓴다. 이 가드가 없으면 "수리했는데 그대로예요"가 반복된다.

server/static/index.html

  • 페이지 하단 <details> "🩹 레이어 수리" 섹션. 펼치면 /drafts를 불러 목록 표시 (⚠ 이름 (꼬임 N개) / ✓ 이름), 선택 후 실행 → 결과 표시 → 목록 갱신.

사용 절차 (중요)

  1. 캡컷에서 그 프로젝트를 닫는다(홈으로). 열어둔 채 수리하면 캡컷이 메모리 상태로 덮어쓴다.
  2. 웹 UI 하단 🩹 레이어 수리 → 드래프트 선택 → 실행.
  3. 캡컷에서 다시 연다.

(a-3) 2026-08-03 — "수리해도 그대로예요"의 진짜 이유: Timelines/

증상: 수리를 눌러 fixed=2 가 나오는데도 캡컷에서 열면 그대로 삐져 있다.

원인: CapCut 9.x(draft_meta_info.jsondraft_new_version = 164.0.0)부터 프로젝트 실데이터가 Timelines/<GUID>/ 아래로 옮겨갔다. repair_layers() 는 루트 draft_content.json 만 고쳤으므로 캡컷이 실제로 읽는 파일은 손대지 않았다.

실측 증거(드래프트 열심히 하는 나경, 소지섭이 올리브를…(2)):

증거 내용
Timelines/ 생성 시점 캡컷에서 한 번이라도 연 드래프트에만 있다. 빌더가 막 만든 드래프트엔 없다 → 그래서 생성은 멀쩡했고 수리만 안 먹었다
루트 파일 없이도 편집됨 소지섭이…(2) 는 루트 draft_content.json아예 없는데 캡컷이 계속 편집 중(Timelines/…/template.json 이 최신) → 루트는 레거시 미러
저장 순서 저장 시각이 항상 template.json 이 가장 늦다
수리 직후 대조 루트만 main ri=[1], Timelines/*/draft_content.json · template.json[1, 4] 그대로

조치 (capcut_agent/draft.py)

이름 변경
timeline_jsons(draft_dir) 신규. 루트 draft_content.json + Timelines/*/draft_content.json + Timelines/*/template.json 목록. .tmp 는 저장 중 임시파일이라 제외
_repair_one(json_path) 신규. 파일 1개 수리(기존 repair_layers 본문). 백업 <파일명>.repair.bak
repair_layers() 위 파일 전부 수리. 반환에 files(고친 파일 수) 추가. 파일마다 내용이 같으므로 fixed 는 합이 아니라 최댓값
_lock_tracks() 사본 전부에 잠금 적용. 바뀔 때만 기록
list_drafts() / _count_bad_ri 루트가 없어도(=Timelines 만 있어도) 목록에 나오고 꼬임을 센다
_registered_drafts() 신규 · list_drafts()path 기본 폴더만 훑으면 안 된다. 캡컷 설정에서 저장 위치를 바꾸면 드래프트가 %LOCALAPPDATA%\CapCut\…\com.lveditor.draft 밖에 생긴다(실측: 222222222222D:/개인폴더/00.유튭/정치/capcut/CapCut Drafts 에 있어 목록에 안 떴다). 어디에 있든 드래프트 루트의 root_meta_info.jsonall_draft_store[].draft_fold_path 가 알고 있다. 목록 항목에 path 를 실어 그걸 수리 키로 쓴다
server/app.py _resolve_draft() 신규 폼 값이 이제 폴더경로basename 고정으로는 못 막는다 → list_drafts() 가 아는 드래프트에만 매칭해 임의 경로 접근 차단(폴더명도 구버전 호환으로 받음)
_draft_title() 신규 · list_drafts()title 캡컷에서 프로젝트 이름을 바꿔도 폴더명은 안 바뀐다(예: 화면 열심히 하는 나경 땜걸~ 찡긋 ↔ 폴더 열심히 하는 나경). 목록이 폴더명만 보여줘서 "내 프로젝트가 목록에 없다"는 착각이 생겼다 → 이름은 root_meta_info.json 우선(캡컷 홈이 읽는 인덱스라 이게 정답. 폴더 안 draft_meta_info.json 엔 옛 이름이 남아 있다 — 실측: 홈 222222222222 ↔ 폴더 메타 언니 괜찮아…22222222222222222222), 없으면 폴더 메타 → 폴더명 순. 폴더명과 다르면 [폴더: …] 를 덧붙인다

검증: 열심히 하는 나경{'fixed': 2, 'detail': {'main': 2}, 'files': 2}, 세 파일 모두 main ri=[1], 재실행 시 fixed=0(멱등). 최종 확인은 캡컷에서 열어봐야 한다.

(a-2) 원복됨 — 레이어 대역 분리 (지금 코드에 없다)

2026-07-30 사용자 요청으로 원복. _apply_layer_bands()는 삭제됐고 CANON_RI{"bg":0,"main":1,"frame":2,"comment":3}로 돌아갔다. 아래는 시도 기록이며 현재 동작이 아니다. 그대로 믿지 말 것. 검증도 못 했다 — 판별용 드래프트가 삭제됐고, CapCut은 접근성 트리가 비어 있어 UI 자동화로도 확인 불가. 재도전하려면 §5 "막다른 길"을 먼저 읽을 것.

(이하 원복 전 기록)

draft.py _apply_layer_bands()build_bg_template_draft() 저장 직후 자동 실행.

bg 0  <  main 1  <<  frame 14500 < comment 14501  <<  자막·제목·효과 24000+
  • CANON_RI = {"bg":0, "main":1, "frame":14500, "comment":14501}, TEXT_RI_MIN = 24000
  • 왜 통하나: CapCut 이 새 비디오 세그먼트에 주는 max+1max비디오 대역 안에서만 계산되면, frame 이 14500 이어도 복붙 클립은 계속 4 를 받아 프레임 아래에 갇힌다.
  • 왜 텍스트를 24000+ 로 올리나: CapCut 렌더 해석이 ①전역 ri 비교든 ②타입별 레이어든 두 경우 모두 순서가 유지되게 하려고. (①이면 frame 14500 > caption 14000 이라 자막이 가려짐)
  • 틀려도 손해 없음: 가정이 틀리면 복붙 클립이 14502 를 받아 예전과 똑같이 동작할 뿐. 그래서 검증 없이 기본값으로 넣었다.
  • repair_layers() 도 같은 대역으로 맞춘다 → 예전 드래프트(frame=2, 텍스트 14000+)를 변환하는 용도로 남는다. 멱등(두 번 돌려도 fixed=0).

검증(실제 드래프트 생성 후 draft_content.json 확인):

트랙 ri
bg / main 0 / 1
frame / comment 14500 / 14501
caption / title_top / title_main / channel / effect 25001 / 25002 / 25003 / 25004 / 25005

(b) 그래도 안 되면 — 판별 근거와 다음 수단

확정된 것: 새 비디오 세그먼트(복붙·이동)는 현재 비디오 최대 render_index + 1 을 받는다. 드래프트 4건에서 재현:

드래프트 비디오 최대 새 클립이 받은 값
핑계고 3015-3240 (이동) 3 (comment) 4
핑계고 3015-3240 (복사) 3 4
눈감고 들으면… (복붙) 3 4
대성 태양 GD… (comment 없음) 2 (frame) 3

(a-2)를 적용한 드래프트에서 복붙 클립이 14502 를 받는다면 "max 는 비디오 전체 대상"이 확정되는 것이고, 그때는 생성 시점 예방이 원리적으로 불가능하다. 그 경우 남는 수단은 편집 마무리 절차(캡컷 완전 종료 → 수리 → 재오픈 → 내보내기)뿐이다.

(b-old) 이전 실험 기록 (미결로 종료)

확정된 것: 새 비디오 세그먼트(복붙·이동)는 현재 비디오 최대 render_index + 1 을 받는다. 드래프트 4건에서 재현:

드래프트 비디오 최대 새 클립이 받은 값
핑계고 3015-3240 (이동) 3 (comment) 4
핑계고 3015-3240 (복사) 3 4
눈감고 들으면… (복붙) 3 4
대성 태양 GD… (comment 없음) 2 (frame) 3

남은 물음: 이 "최대값"이 ① 비디오 트랙의 모든 세그먼트인지, ② ri < 10000(비디오 대역) 세그먼트만인지. ②라면 frame을 대역 밖(14500)에 두면 새 클립은 계속 4를 받아 프레임 아래에 갇힌다 = 영구 해결. ①이라면 14501을 받아 실패 = 예방 불가 확정.

근거: 사용자가 추가한 텍스트는 텍스트 대역 안에서만 증가했고(14109→14114), 복붙한 비디오는 그 14000번대를 무시하고 4를 받았다 → 대역별로 따로 셀 가능성이 있다. 단 비디오 세그먼트가 ri ≥ 10000 인 사례가 기존 드래프트에 0건이라 데이터로는 판별 불가.

판별용 드래프트 __레이어테스트 를 만들어 둠 (frame ri=14500, 텍스트 24000+, 전 트랙 잠금 해제). 여기서 클립 하나 복붙 → 확대 → 삐지는지 보면 끝. 확인 후 이 절을 확정 결론으로 교체하고 테스트 드래프트는 삭제할 것.

CapCut UI 자동화로는 못 푼다: orca computer 로 붙어 봤으나 CapCut 은 접근성 트리가 비어 있고(94자) 스크린샷도 실패한다. 사람이 눌러야 한다.

(c) 문서

  • ARCHITECTURE.md §10 — "잠금으로도 못 막는 경우가 있다(main 트랙)" 항목 추가.
  • SETUP.md 문제 해결표 — "클립을 옮긴 뒤 확대하면 그 클립만 삐짐" 행 추가.

4. 시도해볼 만한 우회

  • 캡컷에서 Ctrl+C/Ctrl+V 대신 우클릭 → 복제. 다른 코드 경로라 렌더순서를 새로 안 찍을 가능성이 있다. 되면 이게 가장 싼 답(코드 변경 불필요). — 미검증

5. 막다른 길 (다시 시도하지 말 것)

  • 마스크: 캡컷 마스크는 클립과 함께 확대/이동하므로 확대 시 같이 커진다 → 클램프 못 함.
  • frame을 텍스트/스티커 트랙으로: StickerSegment는 캡컷 클라우드 스티커 resource_id만 받는다 → 로컬 PNG를 못 올린다.
  • 영상에 흰 띠를 미리 구워 넣기(media.to_template 방식): 확대하면 띠까지 같이 커져 화면 밖으로 나가고 영상이 캔버스를 다 덮는다.
  • 트랙 잠금 확대: main을 잠그면 사용자가 편집을 못 한다.