capcut-agent/작업기록_2026-07-27~31.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

17 KiB
Raw Permalink Blame History

작업 기록 (2026-07-27 ~ 07-31) — 다른 PC 재현 가이드

읽는 대상: 다른 Claude 세션 / 다른 PC에서 같은 상태를 만들려는 사람. 이 문서는 이 기간에 내가 바꾼 것만 다룬다. 앱 전체 구조는 ARCHITECTURE.md, 설치는 SETUP.md, 레이어 삐짐 상세는 레이어_삐짐_수리.md 참고.

⚠ 이 기간에 다른 세션이 별도로 작업한 부분(/auto/*, /prompts, /yt/comments 엔드포인트, index.html 디자인 개편, _load_comment_cards 배치 로직)은 여기 없다. 그건 내 변경이 아니므로 이 문서만 보고 재현하면 안 된다.

바꾼 파일: capcut_agent/draft.py, capcut_agent/pipeline.py, capcut_agent/youtube.py, capcut_agent/media.py, server/app.py, server/static/index.html, build_bg_template.py, ARCHITECTURE.md, SETUP.md.

모든 변경 후 .bat 재시작 필수 (파이썬 코드가 uvicorn에 물려 있어 hot-reload 안 됨).


요약 — 무엇을 왜 바꿨나

# 작업 상태
1 초록 프레임(GOP 중간 컷) 자동 검증·복구 완료·검증됨
2 레이어 삐짐 — 사후 수리 기능 + CapCut 실행중 가드 완료 (근본 예방은 미해결)
3 레이어 대역 분리(생성 시점 예방) 시도 원복함
4 하단 자막 크기 10 고정 완료
5 bg 트랙 잠금 해제 완료
6 템플릿 레이아웃 전면 재조정(상수화) 완료·검증됨
7 백업 파일명 충돌 수정 완료 (버그였음)

1. 초록 프레임 자동 검증·복구 (youtube.py)

증상

합쳐진 영상 중간에 초록 화면이 2초쯤 나온다. 타임라인 썸네일에도 초록으로 보인다 (= 영상 파일 자체에 구워진 것, CapCut 문제 아님).

원인 (실측)

yt-dlp --download-sections가끔 키프레임이 아닌 위치에서 스트림 복사로 잘라, 첫 키프레임 전까지 참조 프레임이 없는 파일을 만든다.

붙여넣기 9컷 중 fe92bcbe1e 파트 → 첫 키프레임이 2.269초 뒤
   병합본의 초록 구간 20.32~22.5s 와 정확히 일치 (파트 시작 20.32 + 2.27)
구간 탭 파일 2개도 깨짐 → 첫 키프레임 2.202s / 1.535s
   그 병합본(merged_809fd228)의 초록: 108.0~110.4s

--force-keyframes-at-cuts가 그 파트에서만 실패해 키프레임 컷으로 폴백한 결과인데, 검증이 없어서 그대로 통과했다.

⚠ 탐지 함정 3종 (이것 때문에 오래 못 잡았다)

  1. 파트를 단독 재생하면 멀쩡하다 — ffmpeg가 깨진 앞부분을 건너뛴다. concat 재인코딩할 때만 초록으로 구워진다.
  2. ffprobe -read_intervals는 못 잡는다 — 키프레임으로 시크해버려 "첫 프레임이 I프레임"이라고 거짓 보고한다. (%+2%+#5가 서로 다른 답을 냈다)
  3. ffmpeg -v error로 디코딩해도 에러가 안 난다 — h264 은닉 처리. 픽셀로만 보인다.

유일하게 확실한 판정: 시크 없이 앞에서부터 프레임을 훑어 첫 키프레임 시각을 본다.

추가한 것

KEYFRAME_TOL = 0.05     # 첫 키프레임이 이보다 늦으면 앞부분 깨짐으로 판정(초)
DL_ATTEMPTS  = 2        # 깨진 결과 재다운로드 횟수(간헐적 실패용)
RECUT_LEAD   = 6.0      # 최후 수단: 앞에 이만큼 여유를 받아 로컬에서 다시 자름(초)
REPAIR_LOG: List[str] = []   # 수리 내역 → 파이프라인이 SSE 로그로 흘림
함수 역할
_first_keyframe_sec(path) 시크 없이 훑어 첫 키프레임 시각(초). 0이면 정상. 첫 키프레임 만나면 즉시 종료
_duration(path) ffprobe 길이
_encode_h264(src, dst, ss, t) h264/aac 재인코딩. ss/t-i에 둬 프레임 정확(출력 시크)
_unlink(path) 조용한 삭제
_dl_section(...) 구간 1회 다운로드(force→폴백) + h264 정규화. tag로 재시도 파일명 분리

동작 (구간 탭 cut_youtube / 붙여넣기 cut_youtube_precise 공통)

  1. 받은 파일의 첫 키프레임이 맨 앞이면 → 통과
  2. 아니면 버리고 재다운로드 (DL_ATTEMPTS=2) — 간헐적이라 대개 여기서 해결
  3. 그래도 깨지면 → 앞에 RECUT_LEAD여유를 붙여 받아 로컬에서 뒤쪽 want초만 재인코딩. 깨진 앞부분은 버리는 여유 구간에 들어가므로 항상 깨끗하다.

수리가 일어나면 웹 UI 로그에 🩹 로 표시된다 (REPAIR_LOGpipeline.py에서 yield).

⚠ Windows 함정 (실제로 터졌다)

_first_keyframe_sec가 ffprobe 파이프를 안 닫으면 파일이 잠겨 바로 뒤 _unlink/덮어쓰기가 PermissionError로 실패한다. → finally에서 stdout.close()kill()wait().

검증 (실제 다운로드로 확인함)

경로 결과
정상 5.505s (요청 5.500s), 첫 키프레임 0.000s
폴백 재컷 강제 5.005s (요청 5.000s), 첫 키프레임 0.033s, 임시파일 잔여 없음

재현 명령:

python -X utf8 -c "
import sys; sys.path.insert(0,'.')
from capcut_agent.youtube import _first_keyframe_sec, _duration
p='.downloads/<파일>.mp4'
print(_first_keyframe_sec(p), _duration(p))   # 첫 값이 0.05 초과면 앞부분 초록
"

이미 받아둔 깨진 영상은 복구 불가 — 다시 만들어야 한다.


2. 레이어 삐짐 — 사후 수리 (draft.py / app.py / index.html)

상세는 레이어_삐짐_수리.md. 여기선 재현에 필요한 것만.

증상

CapCut에서 클립을 복붙하거나 자르고 옮긴 뒤 확대하면, 그 클립만 흰 띠·댓글 위로 올라가 템플릿 밖으로 삐져나온다.

원인

CapCut이 새로 생긴 비디오 세그먼트(복붙·이동)에 렌더순서를 frame(2) 위로 새로 찍는다. 관측값은 대부분 max(비디오 ri)+1(=4)이었으나 3인 반례도 있어 "정확히 max+1"이 보편은 아니다. 확실한 건 frame 위로 재부여된다는 것뿐. 수리는 정상값으로 되돌리는 방식이라 3이든 4든 동일하게 고쳐진다.

main 트랙은 편집해야 하므로 잠글 수 없고, 문제는 그 안에서 일어난다 → 트랙 잠금으로 못 막는다.

추가한 것 — draft.py

이름 역할
CANON_RI = {"bg":0,"main":1,"frame":2,"comment":3} 트랙별 정상 render_index
_count_bad_ri(json_path) 정상값과 다른 비디오 세그먼트 수
list_drafts(draft_root) 드래프트 목록(최근순) + broken/bad
repair_layers(draft_dir) 정상값으로 되돌리고 _lock_tracks 재적용. 멱등

추가한 것 — server/app.py

  • GET /drafts{"drafts":[{name, mtime, broken, bad}, …]} 최근 30개
  • POST /repair (form draft=<폴더명>, force) → {"ok","fixed","detail"}
    • 경로는 os.path.basename으로 정규화(드래프트 루트 밖 접근 차단)
    • _capcut_running()이면 409로 거부 (force=1로만 강행)
  • _capcut_running() = tasklist /FI "IMAGENAME eq CapCut.exe" 파싱

⚠ 이 가드가 왜 필수인가 (실측)

11:04:32  수리 실행 (백업엔 main ri=[1,4])
11:05:39  CapCut이 저장 → 메모리 상태로 덮어써 ri=4 부활

CapCut은 열려 있는 동안 파일을 다시 읽지 않는다. 가드가 없으면 "수리했는데 그대로예요"가 무한 반복된다. 실제로 두 번 반복됐다.

사용 절차 (순서 지켜야 함)

  1. CapCut에서 그 프로젝트를 닫는다 (권장: 프로그램 완전 종료)
  2. 웹 UI 하단 🩹 레이어 수리 → 드래프트 선택 → 실행
  3. CapCut에서 다시 연다

UI — index.html

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


3. 원복함 — 레이어 대역 분리 (생성 시점 예방 시도)

지금 코드에 없다. 다시 넣지 말 것(사용자가 원복 요청).

시도한 것: frame/comment를 비디오 대역 밖(14500/14501)으로 올리고 텍스트를 24000+로 밀기. 가설: CapCut의 max+1비디오 대역 안에서만 계산되면 복붙 클립은 계속 4를 받아 프레임 아래에 갇힌다.

근거는 있었다 — 사용자가 추가한 텍스트는 텍스트 대역 안에서만 증가(14109→14114)했는데, 복붙한 비디오는 그 14000번대를 무시하고 4를 받았다.

결과: 검증 못 하고 원복. 판별용 드래프트를 만들었으나 사용자가 삭제했고, 그 뒤 사용자가 원복을 요청했다. 레이어_삐짐_수리.md의 (a-2) 절은 이 원복을 반영 안 한 상태로 남아 있으니 그대로 믿지 말 것.

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

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

  • 마스크: pycapcut add_mask의 좌표는 "以素材的像素为单位"(소재 픽셀 기준) → 클립과 같이 확대됨. 클램프 불가.
  • frame을 스티커 트랙으로: StickerSegment는 CapCut 클라우드 resource_id만 받음. 로컬 PNG 불가.
  • 영상에 흰 띠 미리 굽기: 확대하면 띠까지 커져 화면 밖으로 나감.
  • main 트랙 잠금: 사용자가 편집을 못 함.

4. 하단 자막 크기 10 고정 (draft.py)

CAPTION_SIZE = 10.0    # 캡컷 폰트 크기와 1:1

기존엔 fit_caption_size()가 최장 줄 기준으로 7~13 사이 자동 결정 → 드래프트마다 크기가 달랐다. 이제 항상 10. 세 탭 공통(같은 빌더).

  • 넘침 없음: 자막 청킹 하드캡이 한 줄 14자, 크기 10이면 한 줄 폭 ≈ 14×51.5 ≈ 721px < 1080.
  • fit_caption_size()지우지 않고 "미사용" 표시만 해뒀다(되돌릴 때 사용).

검증: 실제 드래프트의 draft_content.json에서 size=10.0 확인 (서브 14 / 메인 18은 그대로).


5. bg 트랙 잠금 해제 (draft.py)

LOCK_TRACKS = ("frame", "comment", "title_top", "title_main", "channel")   # "bg" 제거

bg(흰 배경)는 맨 아래 레이어라 순서가 꼬여도 화면에 영향이 없고, 영상 길이를 늘릴 때 같이 늘려야 해서 잠겨 있으면 불편하다.

frame·comment·제목 트랙은 계속 잠근다 — 영상 위 레이어라 쪼개지면 순서가 꼬인다.

검증: 생성 직후 attribute — bg=0, main=0, frame=4, comment=4, caption=0, 제목류=4. (잠금 = attribute 비트2(값 4). mute 비트0은 OR로 보존)

_lock_tracks잠그기만 하고 풀지 않는다. 이미 만들어진 드래프트의 bg는 CapCut에서 자물쇠를 직접 눌러 풀어야 한다.


6. ★ 템플릿 레이아웃 전면 재조정 (pipeline.py / media.py / draft.py)

무엇이 바뀌었나

예전엔 배경.png의 흰 밴드를 자동 감지(detect_white_band)해서 좌표를 잡았다. → 좌표를 정확히 통제할 수 없어서 상수로 바꿨다. 배경.png는 이제 레이아웃에 관여하지 않는다.

조정 지점 — capcut_agent/pipeline.py 상단 (여기만 고치면 전부 따라 움직임)

CANVAS_H      = 1920
VIDEO_TOP     = 323    # 영상 창 시작 = 위 흰 띠가 끝나는 지점
VIDEO_BOTTOM  = 1122   # 영상 창 끝   = 아래 흰 띠가 시작하는 지점
TITLE_TOP_Y   = 109    # 서브제목(주황) 중앙
TITLE_MAIN_Y  = 252    # 메인제목(흰색) 중앙
CAPTION_GAP   = 72     # 하단 자막 중앙 = VIDEO_BOTTOM  이 값
EFFECT_GAP    = 25     # 효과자막 중앙 = VIDEO_TOP + 이 값
COMMENT_TOP   = VIDEO_BOTTOM   # 댓글 카드 윗변 = 영상 바로 아래(딱 붙음)
CHANNEL_RATIO = 0.85   # 출처: 아래 띠에서 85% 내려간 지점

이 값들은 사용자가 준 레퍼런스 템플릿 이미지를 픽셀 측정해서 뽑았다 (레퍼런스 폭 311px → 캔버스 1080px 환산계수 3.4727).

변경 전/후

요소 이전 이후
영상 창 453 ~ 1311 323 ~ 1122
서브제목 y 203 (Y 1513) y 109 (Y 1702)
메인제목 y 344 (Y 1232) y 252 (Y 1416)
하단 자막 y 1240 (Y 559) y 1050 (Y 180)
효과자막 y 478 (Y 965) y 348 (Y 1224)
댓글 카드 중앙 y 1541 고정 윗변 1122 = 영상 바로 아래
출처 y 1829 y 1800

자막·효과자막도 같이 옮겨야 한다 — 안 그러면 새 영상 창 밖으로 나간다.

딸린 변경

media.pymake_frame()band_color 인자 추가(기존엔 검정 하드코딩). _template_posmake_transparent_frame(배경.png 감지) 대신 make_frame(명시 좌표)을 쓴다.

draft.pycomment_top 인자 추가. 댓글 카드 세로 위치를 카드마다 계산한다:

표시높이 = cw × (ih / iw) × COMMENT_SCALE      # COMMENT_SCALE = 0.89
중앙     = comment_top + 표시높이 / 2

카드 이미지 높이가 제각각이라 중앙값 하나로는 "영상 바로 아래"에 못 붙인다. (CapCut scale 1.0 = contain. 댓글 카드는 캔버스보다 가로로 넓어 가로가 먼저 맞음)

pipeline.pycaption_y/effect_y 하드코딩(-559/1920, 965/1920) 제거. 전부 _template_pos()가 상수에서 파생 → 값이 두 군데로 갈라지지 않는다.

build_bg_template.py(단독 CLI) — 자체 좌표 계산을 버리고 _template_pos() 재사용.

검증 (실측)

프레임 PNG 투명 구간: 323 ~ 1121  ✓
main 722.5 / caption 1050.0 / title_top 109.0 / title_main 252.0 /
effect 348.0 / channel 1800.3
댓글 카드 — 높이 다른 두 장(422px, 590px) 모두 윗변 정확히 1122  ✓

재현 명령:

python -X utf8 -c "
import sys; sys.path.insert(0,'.')
from capcut_agent.pipeline import _template_pos
frame,bg,pos=_template_pos(True)
for k,v in pos.items():
    print(k, v, '' if k=='comment_top' else f'→ 캔버스y={960-960*v:.1f}px, 캡컷Y={round(v*1920)}')
"

7. 백업 파일명 충돌 수정 (버그였음)

repair_layers의 백업을 처음에 draft_content.json.bak으로 만들었는데, 그게 CapCut 자체 백업 파일명이었다. 손대지 않은 프로젝트에도 전부 존재한다. → 내 수리가 CapCut 백업 2개를 덮어썼다(프로젝트 자체엔 영향 없음).

draft_content.repair.bak 으로 변경. draft_content.json.bak은 절대 쓰지 말 것.


다른 PC에서 재현하는 순서

  1. 선행 조건 확인SETUP.md 기준. PATH에 ffmpeg/ffprobe, yt-dlp, Node.js 또는 deno(yt-dlp JS 런타임). CapCut 설치 + 코트라 볼드체 캐시(없으면 기본 폰트로 안전 동작).
  2. 1·2·4·5·6·7을 순서대로 적용. 3번은 적용하지 말 것(원복된 실험).
  3. .bat 재시작.
  4. 아래 검증 전부 통과 확인.

검증 명령 모음

# 구문 + 임포트 체인
python -X utf8 -c "
import ast
for f in ['capcut_agent/draft.py','capcut_agent/pipeline.py','capcut_agent/youtube.py',
          'capcut_agent/media.py','server/app.py','build_bg_template.py']:
    ast.parse(open(f,encoding='utf-8').read())
print('구문 OK')
from server import app; print('임포트 체인 OK')
"

# 상수 확정값
python -X utf8 -c "
import sys; sys.path.insert(0,'.')
from capcut_agent.draft import CAPTION_SIZE, LOCK_TRACKS, CANON_RI, COMMENT_SCALE
from capcut_agent.pipeline import VIDEO_TOP, VIDEO_BOTTOM, TITLE_TOP_Y, TITLE_MAIN_Y
from capcut_agent.youtube import KEYFRAME_TOL, DL_ATTEMPTS, RECUT_LEAD
print(CAPTION_SIZE, LOCK_TRACKS, CANON_RI, COMMENT_SCALE)
print(VIDEO_TOP, VIDEO_BOTTOM, TITLE_TOP_Y, TITLE_MAIN_Y)
print(KEYFRAME_TOL, DL_ATTEMPTS, RECUT_LEAD)
"

기대값

10.0 ('frame','comment','title_top','title_main','channel') {'bg':0,'main':1,'frame':2,'comment':3} 0.89
323 1122 109 252
0.05 2 6.0

실제 드래프트로 최종 확인

테스트 드래프트를 만들어 draft_content.json을 열어보고 확인 후 반드시 삭제한다 (shutil.rmtree). 이 프로젝트엔 자동 테스트 스위트가 없고, 최종 검증은 사용자가 CapCut에서 열어보는 방식이다.


미해결로 남은 것

  1. 레이어 삐짐 근본 예방 — 생성 시점 예방책 없음. 현재는 사후 수리뿐. 재도전하려면 §3의 막다른 길 목록을 먼저 읽을 것.
  2. 제목이 안 보인다는 보고(2026-07-29) — 파일상으로는 정상이었다. 내용·크기·색·폰트·시간범위·visible=true·위치 전부 확인했으나 미리보기에만 안 나왔다. 원인 미규명. 재발하면 트랙의 👁(눈) 아이콘 상태부터 확인할 것.
  3. 영상보다 오버레이가 짧아지는 문제 — 클립을 뒤에 추가하면 frame·제목 트랙은 생성 당시 길이에서 끝나 그 뒤 구간엔 템플릿이 아예 없다(실측: 영상 44.13s vs 프레임 33.97s). 자물쇠를 풀고 같이 늘려야 한다. 자동화 안 됨.