recommend.py 모듈, 카드 배치가 자동 탭(card_cuts→_cards_by_cut)과 나머지 탭(_load_comment_cards 전체 균등)으로 갈린 이유, 그리고 서버가 카드 시간을 미리 확정하지 않는 이유(무음 제거 시 자막만 재매핑되고 카드가 혼자 어긋나는 것을 방지)를 문서에 남겨 나중에 이 설계를 실수로 되돌리는 것을 막는다. SETUP.md 문제 해결표에도 추천 실패 폴백 증상 두 건을 추가. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
25 KiB
캡컷 에이전트 · 구간합치기 (capcut2) — 시스템 명세
이 문서는 다른 Claude 세션(또는 개발자)이 이 프로젝트를 읽고 바로 협업할 수 있게 쓴 전체 구현 명세입니다. 사용자용 요약은 README.md 참고. 이 문서가 더 깊고 정확합니다.
0. 한 줄 요약
유튜브 영상(URL/파일/LLM 편집안 JSON) → 다운로드·컷·자막·배경 템플릿을 자동 조립해
편집 가능한 CapCut 드래프트(draft_content.json)를 로컬 CapCut 프로젝트 폴더에 생성하는
FastAPI + 정적 HTML 로컬 웹앱. 포트 8001 (v1 ../capcut은 8000, 별개 앱).
실행: 캡컷_에이전트_구간합치기.bat → uvicorn server.app:app → http://127.0.0.1:8001
코드 수정 후엔 반드시 .bat 재시작 (파이썬 코드가 서버에 물려 있음).
1. 폴더 구조
capcut2/
├─ 캡컷_에이전트_구간합치기.bat 런처(포트 8001, 브라우저 자동 오픈)
├─ 배경.png 배경 템플릿(검정-흰-검정 세로 1080×1920).
│ (레이아웃엔 더 이상 안 씀 — pipeline.py 상수로 통제)
├─ 댓글카드/ 댓글 카드 이미지 폴더(기본 경로, UI에 자동 주입)
├─ requirements.txt fastapi uvicorn python-multipart pyCapCut Pillow
│ pymediainfo yt-dlp faster-whisper
├─ .gemini_key (선택) Gemini API 키 — 파일/유튜브 탭 자막 교정용
├─ assets/ 파생물(frame_template.png 등) 자동 생성
├─ server/
│ ├─ app.py FastAPI. 엔드포인트 4개 + SSE 스트림
│ └─ static/index.html UI 전체(단일 파일, 탭 3개 + 옵션 + SSE 렌더)
└─ capcut_agent/
├─ pipeline.py ★ 두 파이프라인(process_bg_template / process_paste)
├─ draft.py ★ CapCut 드래프트 생성(pycapcut) + JSON 후처리
├─ youtube.py yt-dlp 다운로드(단일/다중/정밀) + ffmpeg 병합
├─ paste.py 붙여넣기 JSON 파서(관대한 파싱)
├─ silence.py ffmpeg silencedetect → 발화 구간
├─ transcribe.py faster-whisper(medium/int8/cpu) 단어 타임스탬프
├─ correct.py Gemini 자막 글자 교정(시간 불변) — gemini-2.5-flash
├─ recommend.py 컷별 댓글 추천(자동 탭) — 타임스탬프 우선 + Gemini 텍스트 추천
├─ highlight.py 자막 청킹(cut_plan) 유틸
├─ scene.py ffmpeg scene 필터 장면전환 감지·분할
├─ media.py 프레임 PNG 생성, 흰밴드 감지, 오디오 추출
└─ probe.py pymediainfo 영상 메타
2. 좌표계 (중요)
CapCut 인스펙터 값 ↔ pycapcut 변환:
- 위치 Y:
CapCut Y = transform_y × 1920(canvas 1080×1920 기준). 위가 +, 아래가 −. 예) transform_y = −559/1920 = −0.2911 → 인스펙터에 Y −559로 표시. - 확대(%):
scale_x = scale_y = 비율(0.89 → 89%). - 글자 크기: pycapcut TextStyle.size ≈ CapCut 폰트 크기 1:1.
- 헬퍼
_ty(y_px) = (960 - y_px) / 960(draft.py) — 픽셀 y → transform_y.
3. 서버 (server/app.py)
| 엔드포인트 | 역할 |
|---|---|
GET / |
index.html 서빙. __CDIR__ 토큰을 그 PC의 capcut2/댓글카드 절대경로로 치환 |
POST /upload |
파일 탭. multipart 파일 + 옵션 → job 등록(content-hash id) |
POST /youtube |
유튜브 탭. url + ranges(JSON [["mm:ss","mm:ss"],…]) + 옵션 |
POST /paste |
붙여넣기 탭. data(편집안 JSON 문자열) + 옵션 |
GET /stream/{job_id} |
SSE. job 종류에 따라 파이프라인 실행, 이벤트 스트림 |
POST /open-capcut |
CapCut 실행(시작메뉴 lnk → LOCALAPPDATA exe 폴백) |
- job 은 메모리 dict
JOBS[hash]. hash = 입력 시그니처 sha1 12자. - SSE 이벤트 형식:
{"type": "manifest"|"step"|"log"|"error"|"result", ...}manifest:{steps:[{id,label}]}/step:{id,status:"start"|"done",elapsed,detail}result:{draft_name, draft_path, stats:{duration,kept,cut,segments,captions,elapsed}}
- 공통 폼 필드:
video_scale(% 문자열, 기본 144),flip,scene,bg_white,comments_dir(폴더 경로 문자열),title_top/title_main/channel(파일·유튜브만),remove_silence(붙여넣기만). 불리언은 "1"/"0" 문자열 →_truthy().
4. 탭별 파이프라인
4-A. 📁 파일 / ▶ 유튜브 구간 → process_bg_template() (pipeline.py)
단계: [download] → silence → asr → [scene] → draft
- download (유튜브만):
cut_youtube_multi(url, ranges, out)— 구간별로yt-dlp --download-sections(h264 우선) 다운로드 → 구간 2개 이상이면 ffmpeg concat demuxer로 재인코딩 병합(libx264 crf20 + aac). 채널명 자동 추출 → 출처(channel) 미입력 시@채널명자동. - silence:
detect_speech_segments(noise_db=-28, min_silence=0.3, pad=0.04)— ffmpeg silencedetect의 여집합 = 발화 구간keep=[(s,e)]. 이게 곧 비디오 컷. - asr: 타이밍 = Whisper, 글자 = Gemini 제자리 교정 (핵심 설계):
- faster-whisper(medium/int8/cpu,
word_timestamps=True, vad_filter=False, no_repeat_ngram_size=3)로 단어 타임스탬프 →cut_plan()이 단어를 새(컷 압축) 타임라인으로 매핑 후 짧은 자막으로 청킹. - 청킹 = DP 줄바꿈 최적화(
_chunk_words). 0.6s 이상 쉬는 곳으로 덩어리를 나눈 뒤, 덩어리 안에서Σ(줄길이−10)² + 끊는 자리 벌점이 최소가 되는 줄바꿈을 고른다. 벌점: 다음 어절이 의존명사·보조용언(_is_bound: "수/것/번도/들어…") +60, 앞 어절이 관형형·관형사(_is_adnominal/_DETERMINERS: "쓸/당황하는/한") +50, 문장부호·어미로 끝나면 보너스. 하드캡 14자(fit_caption_size한 줄 폭 한계)/2.8s. ⚠ 앞에서부터 12자 차면 무조건 끊던 greedy 방식은 "예를 / 들어", "쓸 / 수 있잖아요" 처럼 한 덩어리를 갈랐다(실측 자막 경계의 8.5% → DP 적용 후 0.7%). ★ DP는 묶는 방법만 고른다 — 단어 타임스탬프는 손대지 않으므로 싱크 불변. .gemini_key있으면correct_captions()로 글자만 1:1 교정(줄 수·순서·시간 절대 불변 → 싱크 유지). 실패 시 Whisper 원문 유지.- ⚠ 과거 시도: Gemini 오디오 전사 타임스탬프 직접 사용 → 드리프트로 자막 밀림. 글자수 기반 정렬도 누적 드리프트. 절대 되돌리지 말 것.
- ASR은
.cache/에 content-hash 캐시. numba 동시 호출 segfault →ASR_LOCK.
- faster-whisper(medium/int8/cpu,
- scene (옵션):
detect_scene_changes(threshold=0.4)→split_clips_at_scenes()— 보존 구간을 장면전환 지점에서 인접 분할(누적 길이 불변 → 자막 싱크 무영향). - draft: §5 빌더 호출. 좌표는 전부
_template_pos()가 레이아웃 상수에서 파생(§9).
4-B. 📋 붙여넣기 (기본 탭) → process_paste() (pipeline.py)
LLM이 만든 편집안 JSON을 그대로 사용. 무음컷·ASR 기본 없음(옵션으로 무음 제거 가능).
단계: download(컷 정밀) → [remove_silence] → [scene] → draft
- 파싱 (
paste.parse_paste): 관대한 JSON 파싱 —json.loads(strict=False)(자막 안 실제 줄바꿈 허용), 코드펜스(```) 자동 제거, 시간은parse_time():"분:초.밀리"/"시:분:초.밀리"/초 단독, 콤마 밀리초도 허용. 검증 실패 시 한국어 메시지로 400. - download:
download_paste_cuts()— 컷마다cut_youtube_precise():--download-sections "*HH:MM:SS.mmm-…" --force-keyframes-at-cuts(프레임 정확). 실패 시 keyframe 컷으로 자동 폴백(일부 영상에서 ffmpeg 크래시 방어). 전부 받은 뒤 concat 재인코딩 병합. 제목/채널 자동 추출. - 자막 배치: 배치 시간은 컷 순서 누적 자동 계산 (LLM이 배치시간 계산하면
오타 나므로 입력받지 않음).
bottom→ 하단 자막,effect→ 중앙 효과자막. - remove_silence (옵션, 기본 꺼짐): 병합본에서 무음 감지 → 컷 추가 압축,
자막을
_remap_caps()로 압축 타임라인에 재매핑(무음에만 걸친 자막은 버림). - asr_bottom (옵션, 기본 켜짐): 병합본을 Whisper로 받아써 JSON bottom 을 대체하는
하단 자막 생성(실제 발화 타이밍). 구현 핵심: Whisper 타임스탬프는 '병합 파일' 기준이므로
cut_plan(video_clips, tr)로 매핑+청킹 — 무음제거 켜면 video_clips=keep(압축 매핑), 끄면 (0,dur)..gemini_key있으면 글자만 1:1 교정(시간 불변). ASR 결과가 비면 JSON bottom 폴백. effect/제목/채널은 JSON 유지. ⚠ "remove_silence 후에 돌리면 remap 불필요"는 틀림 — 파일은 압축 안 되므로 cut_plan 매핑 필수. - scene / 댓글카드 / draft: 4-A와 동일(좌표도 공통 — §9).
자동 탭에서 넘어온 경우
process_paste(card_cuts=[...])로 카드별 소속 컷을 받는다(§6 참고) — 붙여넣기 탭 직접 사용 시엔 생략(기존 전체 균등 배치).
붙여넣기 JSON 스키마 (LLM에게 시킬 형식)
{
"url": "https://www.youtube.com/watch?v=실제영상ID",
"title_top": "서브제목(주황)", "title_main": "메인제목(흰색)", "channel": "@채널",
"cuts": [
{"start":"16:07.500","end":"16:12.500",
"bottom":"하단 자막 윗줄\n하단 자막 아랫줄","effect":"(효과자막)"}
]
}
url: 반드시 실제 영상. LLM이 지어낸 가짜 ID(oembed 404)로 실패한 전례 있음.start/end: 원본 영상 기준. 배치 시간·SRT 타임코드는 받지 않는다.bottom의\n: 같은 위치에서 시간을 줄 수만큼 균등 분할해 순서대로 표시 (윗줄 → 아랫줄). 위아래 스택이 아님. LLM이\\n(이중 이스케이프)으로 줘도, 글자 그대로\n/\r이 와도 draft.py_cap_lines()가 전부 줄바꿈으로 처리.title_*/channel/bottom/effect전부 선택(빈 값이면 생략).
LLM 지시문 템플릿
아래 스키마의 JSON 하나로만 출력해. 설명·마크다운 금지. url은 내가 준 이 주소 그대로(임의 생성 금지). start/end는 원본 타임스탬프(분:초.밀리). 배치 시간은 계산하지 마라(앱이 순서대로 이어붙임). bottom은 2줄(\n), effect는 짧게. 모든 컷 start < end.
5. 드래프트 빌더 (draft.py build_bg_template_draft)
캔버스 1080×1920. 트랙 구성(아래→위 렌더 순):
[bg] (배경 흰색일 때만) 흰 단색 1080×1920 PNG, 전체 길이
main 영상. 보존 구간들을 이어붙임(점프컷). scale=video_scale, flip, video_y=영상 창 중앙
frame make_frame() 생성 — 상하 띠 불투명(검정 or 흰), 가운데 투명(영상 비침). 좌표는 §9 상수
[comment] 댓글 카드 이미지들 — scale 0.89 고정, X 0, 윗변=영상 바로 아래, 카드당 3초
caption 하단 자막(텍스트)
title_top 서브제목 / title_main 메인제목 / channel 출처 / effect 효과자막
핵심 구현 포인트:
- 소재 길이 클램프: ffprobe duration이 CapCut 소재 길이보다 수십 ms 길 수 있음
→ 컷 끝을
material.duration으로 클램프(SegmentOverlap/초과 오류 방어). - 자막 스타일: 주황
#ff8000(CAPTION_COLOR) + 볼드 + 그림자, 배경박스 없음, 크기CAPTION_SIZE = 12.0고정(캡컷 폰트 크기 1:1). 배경은TextSegment(background=...)인자를 생략해서 끈다 →background_style키 자체가 안 나가고 CapCut 이 읽으면서 0(없음)으로 채운다(제목 텍스트가 원래 이 방식). 그림자는 저장 후_apply_shadow_to_track(draft_dir, "caption", _TEXT_SHADOW)로 주입. 예전엔 최장 줄 기준fit_caption_size()로 7~13 자동이었으나 드래프트마다 크기가 달라져 고정으로 바꿈. 청킹 하드캡 14자 × 크기10(≈51.5px) ≈ 721px < 1080 → 안 넘침.fit_caption_size()는 남겨둠(미사용, 자동 맞춤으로 되돌릴 때 사용). \n시간분할: n줄이면 구간을 n등분해 각 줄을 같은 위치(caption_y)에 순차 표시.- 제목: title_top 주황
(1.0,0.62,0.05)size14 bold 검은외곽선18 / title_main 흰색 size18 bold. 위치는 §9. 전체 길이 표시. - 효과자막: 녹색
#0dff63size13 bold 아님 검은외곽선18 (붙여넣기 전용). - 배경 흰색 모드(
bg_white=True): 흰 띠 프레임 + 흰 배경 레이어. 추가로 channel 글자 검정, title_main 외곽선 두께 50, title_top에 그림자 JSON 주입 (_apply_shadow_to_text, CapCut 실측값: alpha .9, diffuse .025, distance 5, angle −45). - ⚠ 그림자는 두 군데를 같이 넣어야 켜진다:
styles[].shadows(_TEXT_SHADOW) 와 소재 최상위 플래그(_SHADOW_MATERIAL:has_shadow=true,shadow_color,shadow_alpha,shadow_angle,shadow_distance,shadow_point,shadow_smoothing).shadows만 넣으면 JSON 엔 있는데has_shadow=false라 화면엔 안 나온다 — 기존 title_top 그림자가 딱 이 상태였고(실측 확인) 같이 고쳤다. 값은 CapCut UI 로 켠 자막에서 실측한 것. - 폰트: 코트라 볼드체 — pycapcut FontType에 없어 저장 후 draft_content.json의
모든 텍스트 styles[].font에 직접 주입(
_apply_font_to_texts). 경로는%LOCALAPPDATA%/CapCut/User Data/Cache/effect/7480846567709265157/...(PC 무관). 캐시 없으면(그 PC CapCut에서 폰트 미사용) 주입 생략 → 기본 폰트로 안전 동작. - 트랙 자동 잠금(
_lock_tracks, 저장 직후):frame·comment·title_top·title_main·channel트랙을 잠근다(attribute |= 4; mute 비트1은 OR로 보존).main(영상)·caption(자막)·effect는 편집용이라 안 잠금.bg(흰 배경)도 안 잠금 — 맨 아래 레이어라 꼬여도 화면에 영향이 없고, 영상 길이를 늘릴 때 같이 늘려야 해서 잠겨 있으면 불편하다(사용자 요청으로 제외). 파일/유튜브/붙여넣기 전 탭 공통 — 세 탭 모두 이 빌더(build_bg_template_draft)를 쓰므로 자동 적용.- ⚠ 왜 잠그나(중요): CapCut에서 재생헤드 전체 분할 등으로 오버레이 트랙이 쪼개지면
CapCut이 세그먼트
render_index를 다시 매기다 일부 main 영상 세그먼트를 frame/comment 위로 올려버려(예: main ri=4 > frame ri=2) 그 클립이 흰 띠·댓글을 덮고 삐져나오는 버그가 있었다. 오버레이/제목 트랙을 미리 잠그면 분할·재배치가 막혀 레이어가 안 꼬인다. render_index는 시간 무관 전역 쌓임 순서(클수록 위). 정상 생성물은 bg=0<main=1< frame=2<comment=3 으로 일관. 이미 꼬인 드래프트는 각 트랙 세그먼트 render_index를 트랙별 단일값으로 재통일하면 복구된다(repair_layers()).- 잠금 인코딩은 CapCut 실측 확인값(트랙
attribute비트4=잠금, 비트0=mute).
- ⚠ 왜 잠그나(중요): CapCut에서 재생헤드 전체 분할 등으로 오버레이 트랙이 쪼개지면
CapCut이 세그먼트
- 저장 위치:
%LOCALAPPDATA%/CapCut/User Data/Projects/com.lveditor.draft/<드래프트명>/ - ⚠ draft_content.json이 정답 파일(draft_info.json 아님). 같은 트랙에 같은
시간대 세그먼트 2개 넣으면
SegmentOverlap에러.
6. 댓글 카드 시스템
- UI "댓글 카드 폴더" 칸(기본값 =
capcut2/댓글카드, 서버가 실제 경로 주입). 비우면 카드 없음. _load_comment_cards(folder, dur, interval=3.0):- 모든 파일명이 숫자로 시작 → 숫자순(1,2,10). 아니면 → 파일 생성시각(저장 순서).
- png/jpg/jpeg/webp. 카드당 3초, 영상 길이 초과분은 생략.
- 배치 방식이 두 갈래(정렬 자체는
_card_paths()로 공통): 자동 탭은 검토 화면에서 카드별 소속 컷 인덱스(card_cuts)를 보내고,_cards_by_cut(paths, card_cuts, placements, dur)가 그 컷 구간 안에서 균등 배치한다(한 컷이 덜 차도 다음 컷 카드가 앞으로 밀리지 않음). 파일/유튜브 탭·붙여넣기 탭 직접 사용은 컷 소속을 몰라 기존_load_comment_cards전체 균등 배치 그대로 쓴다.- ⚠ 카드 시간은 서버가 미리 확정하지 않는다.
/auto/build는 "몇 번 컷 소속"만 넘기고, 파이프라인이 컷 누적 위치(placements)로 시간을 계산한다. 무음 제거를 켜면 타임라인이 압축되는데(timeline_dur = sum(keep)), 서버가 시간을 미리 박아두면 자막만 재매핑되고 카드는 혼자 어긋나기 때문이다 — 카드도 자막과 **같은_remap_caps()**로 재매핑된다 (튜플 모양이(start, end, path)로 같아서 가능). 이 순서를 뒤집지 말 것.
- ⚠ 카드 시간은 서버가 미리 확정하지 않는다.
- 렌더: comment 트랙에 scale 0.89 / X 0, 세로는 윗변이 영상 바로 아래에 오도록 카드마다 계산(§9).
- 출처: 사용자가 h-lab(https://h-lab.tolag.shop/comment-cards)에서 실제 유튜브 댓글을 카드 PNG로 저장해 폴더에 넣음. (향후: h-lab API 연동해 완전 자동화 아이디어 있음)
7. yt-dlp 관련 (youtube.py) — 함정 모음
- JS 런타임 필수: 최신 유튜브는 JS 챌린지 필요.
_js_runtime_args()가 deno→node→bun 순으로 자동 감지해--js-runtimes지정. 없으면 포맷 누락 → 다운로드된 스트림으로 ffmpeg가 크래시(exit 3436169992)했던 전례. - 포맷:
bv*[vcodec^=avc1]+ba[acodec^=mp4a]/…/b(h264+aac 우선, CapCut 호환). 받은 게 h264 아니면 ffmpeg 재인코딩. - 한글 경로:
PYTHONIOENCODING=utf-8env + stdout 파싱 대신 glob으로 결과 파일 탐색 (Windows cp949 디코드 깨짐 방어). 모든 subprocess는encoding="utf-8", errors="replace". - 정밀 컷:
--force-keyframes-at-cuts1차 → 실패 시 keyframe 컷 폴백. - ⚠ 초록 화면(GOP 중간 컷) — 반드시 검증할 것: 위 폴백이 걸리면 yt-dlp가
키프레임이 아닌 위치에서 스트림 복사로 잘라, 첫 키프레임 전까지 참조 프레임이 없는
파일이 나온다(실측: 9컷 중 1개, 첫 키프레임 2.27s). 그대로 concat 재인코딩하면
그 구간이 통째로 초록 화면으로 구워진다.
- 탐지 함정 3종: ① 파트를 단독 재생하면 ffmpeg가 깨진 앞부분을 건너뛰어 멀쩡해 보인다.
②
ffprobe -read_intervals는 키프레임으로 시크해버려 첫 프레임을 놓친다. ③ffmpeg -v error로 디코딩해도 에러가 안 난다(h264 은닉 처리). 픽셀로만 보인다. - 유일한 확실한 판정: 시크 없이 앞에서부터 프레임을 훑어 첫 키프레임 시각을 본다
→
_first_keyframe_sec(). 0이 아니면 그만큼 앞이 깨진 것. - 대응(
cut_youtube/cut_youtube_precise공통): 검증 실패 → 재다운로드 (DL_ATTEMPTS=2, 간헐적이라 보통 여기서 해결) → 그래도 깨지면 앞에RECUT_LEAD=6초 여유를 붙여 받아 로컬에서 뒤쪽 want초만 재인코딩해 잘라낸다(깨진 앞부분은 버리는 여유 구간에 들어가므로 항상 깨끗). 수리 내역은REPAIR_LOG→ SSE 로그(🩹)로 노출. - ⚠ Windows:
_first_keyframe_sec의 ffprobe 파이프를 안 닫으면 파일이 잠겨 바로 뒤_unlink가 PermissionError로 실패한다 →stdout.close()후 kill/wait.
- 탐지 함정 3종: ① 파트를 단독 재생하면 ffmpeg가 깨진 앞부분을 건너뛰어 멀쩡해 보인다.
②
- "Video unavailable" 디버깅:
curl "https://www.youtube.com/oembed?url=...&format=json"이 404면 yt-dlp 문제가 아니라 영상 자체가 없는 것(LLM이 지어낸 ID 등).
8. UI (index.html) 요약
- 탭 3개: 파일 / 유튜브 구간 / 붙여넣기(기본 활성). 붙여넣기 탭에선 제목 입력칸 숨김(JSON에 있으므로), 옵션들은 노출.
- 유튜브 탭: "+ 구간 추가"로 구간 여러 개(각 행 시작/끝, ✕ 삭제). 시간 자동 포맷 (4314→43:14), 끝 비우면 시작+90초.
- 옵션(공통): 영상 확대 슬라이더(기본 144%) / 댓글 카드 폴더 / 좌우반전 / 장면분할(기본 체크) / 배경 흰색(기본 체크) / 무음 제거(붙여넣기용, 기본 꺼짐).
- 헤더 우측 고정 링크: ✨ AI Studio(aistudio.google.com), 💬 댓글 카드(h-lab).
- 완료 시 결과 카드(총 소요시간 포함) + "완료되면 CapCut 자동 실행" 체크.
9. 현재 고정값 치트시트
레이아웃은 pipeline.py 상단 상수 한 곳에서 파생된다 — 여기만 고치면 전부 따라 움직인다.
(예전엔 배경.png 흰밴드 자동감지였으나 좌표를 정확히 통제하려고 상수로 바꿨다.
배경.png는 더 이상 레이아웃에 관여하지 않는다.)
VIDEO_TOP = 323 VIDEO_BOTTOM = 1122 # 영상 창 (흰 띠 사이)
TITLE_TOP_Y = 109 TITLE_MAIN_Y = 252 # 제목 두 줄 중앙
CAPTION_GAP = 72 EFFECT_GAP = 25 # 영상 창 안쪽 아래/위 여백
COMMENT_TOP = VIDEO_BOTTOM # 댓글 카드 윗변 = 영상 바로 아래
CHANNEL_RATIO = 0.85 # 아래 띠에서 85% 지점
| 항목 | 캔버스 y(px) | transform_y | CapCut 표시 |
|---|---|---|---|
| 영상 창(투명 구간) | 323 ~ 1122 | — | — |
| 영상(main) 중앙 | 722.5 | 0.2474 | Y 475 |
| title_top (주황 size14) | 109 | 0.8865 | Y 1702 |
| title_main (흰색 size18) | 252 | 0.7375 | Y 1416 |
| 효과자막 (#0dff63 size13, bold X) | 348 | 0.6375 | Y 1224 |
| 하단 자막 (size 12, #ff8000, 그림자, 배경 없음) | 1050 | −0.0938 | Y −180 |
| 댓글 카드 (scale 0.89, X 0, 3초/장) | 윗변 1122 | 카드마다 계산 | — |
| channel (size 10) | 1800 | −0.8753 | Y −1681 |
| 영상 확대 기본 | — | — | 144% |
- 댓글 카드 세로 위치는 카드마다 다르게 계산된다 — 카드 이미지 높이가 제각각이라
중앙값 하나로는 "영상 바로 아래"에 못 붙인다.
표시높이 = 1080 × (h/w) × 0.89,중앙 = COMMENT_TOP + 표시높이/2. (comment_top인자, 실측: 422px/590px 카드 모두 윗변 1122)
10. 알려진 제약 / 하지 말 것
- 코드 수정 후 .bat 재시작 필수 — 안 하면 옛 코드가 계속 돎(가장 흔한 "안 돼요" 원인).
- Gemini 오디오 전사의 타임스탬프를 자막 타이밍으로 쓰지 말 것(드리프트). 타이밍은 Whisper 단어 타임스탬프만.
- 같은 텍스트 트랙에 동시간 세그먼트 2개 금지(SegmentOverlap).
- 오버레이(frame/comment)·제목 트랙은 생성 시 자동 잠금(§5). 이 잠금을 빼지 말 것 —
CapCut 편집 중 render_index 재배치로 영상이 프레임 위로 삐지는 버그의 예방책. 편집하다
제목을 고쳐야 하면 그 트랙 자물쇠만 잠깐 푼다. (
bg는 잠그지 않는다 — 맨 아래라 무해) - ⚠ 잠금으로도 못 막는 경우 —
main트랙(미해결): main 은 편집용이라 잠글 수 없고, CapCut 은 새로 만든 비디오 세그먼트(복붙·분할 후 이동) 에max(비디오 render_index)+1을 새로 찍는다(실측 4건: 최대 3 → 4, 최대 2 → 3). 그 클립만 frame·comment 위로 올라가 확대 시 흰 띠·댓글 위로 삐진다.- 생성 시점 예방은 미해결. frame 을 비디오 대역 밖(14500)으로 올리는 방식을 시도했다가
원복했다(사용자 요청). 근거·재시도 조건은
레이어_삐짐_수리.md참고. - 현재 수단은 사후 수리:
draft.repair_layers(draft_dir)— 트랙별 정상 render_index (bg0/main1/frame2/comment3)로 되돌리고 재잠금,draft_content.repair.bak백업. UI 하단 "🩹 레이어 수리" /GET /drafts(꼬임 감지) ·POST /repair. - ⚠
POST /repair는 CapCut 실행 중이면 409 로 거부 — 열어둔 채 수리하면 CapCut 이 메모리 상태로 덮어써 되돌아간다(실측: 11:04:32 수리 → 11:05:39 CapCut 저장으로 원복).
- 생성 시점 예방은 미해결. frame 을 비디오 대역 밖(14500)으로 올리는 방식을 시도했다가
원복했다(사용자 요청). 근거·재시도 조건은
JOBS는 메모리 저장 — 서버 재시작하면 job 소실(스트림 전에 재시작하면 재제출 필요).- 검증은 최종적으로 사용자가 CapCut에서 열어 확인하는 방식.
- v1(
../capcut, 포트 8000)은 별개 코드베이스 — 여기 수정해도 v1에 반영 안 됨(역도 동일).
11. 협업 시 참고
- 파이썬 검증 습관:
python -c "import ast; ast.parse(open(f,encoding='utf-8').read())"→from server import app임포트 확인 → 가능하면 실제 드래프트 빌드 후 draft_content.json을 열어 값 검증(테스트 드래프트는 빌드 후 삭제). - Windows 콘솔은 cp949라 한글/특수문자 print가 깨져 보일 수 있음(로직과 무관).
- 사용자의 CapCut 좌표 요청("위치 −559로") = transform_y로 환산해 반영하면 됨(§2).
- 테스트용 영상이 필요하면
.downloads/의 기존 mp4를 재사용(네트워크 불필요).