commit bf1b387d6d50f12d07a8974e8c1831a2952f0f0f Author: hehihoho3@gmail.com Date: Tue Aug 4 11:36:05 2026 +0900 chore: git 저장소 초기화 (기존 코드 스냅샷) 컷별 댓글 추천 작업을 태스크 단위로 되돌릴 수 있게 버전관리를 시작한다. .gitignore 로 영상·캐시(.downloads 2.7G, .comments 72M, .media 28M)와 비밀키(.gemini_key)를 제외했다. Co-Authored-By: Claude Opus 5 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..932dfcf --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# 비밀키 — 절대 커밋 금지 +.gemini_key + +# 작업 산출물·캐시 (용량이 크고 재생성됨) +.downloads/ +.comments/ +.uploads/ +.media/ +.cache/ +댓글카드/ + +# 파이썬 +__pycache__/ +*.pyc + +# 서브에이전트 작업 공간 (superpowers SDD) +.superpowers/ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..d81f564 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,330 @@ +# 캡컷 에이전트 · 구간합치기 (capcut2) — 시스템 명세 + +> 이 문서는 다른 Claude 세션(또는 개발자)이 이 프로젝트를 읽고 바로 협업할 수 있게 쓴 **전체 구현 명세**입니다. +> 사용자용 요약은 [README.md](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 + ├─ 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` + +1. **download** (유튜브만): `cut_youtube_multi(url, ranges, out)` — + 구간별로 `yt-dlp --download-sections`(h264 우선) 다운로드 → 구간 2개 이상이면 + ffmpeg concat demuxer로 **재인코딩 병합**(libx264 crf20 + aac). 채널명 자동 추출 + → 출처(channel) 미입력 시 `@채널명` 자동. +2. **silence**: `detect_speech_segments(noise_db=-28, min_silence=0.3, pad=0.04)` — + ffmpeg silencedetect의 여집합 = 발화 구간 `keep=[(s,e)]`. 이게 곧 비디오 컷. +3. **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`. +4. **scene** (옵션): `detect_scene_changes(threshold=0.4)` → `split_clips_at_scenes()` — + 보존 구간을 장면전환 지점에서 **인접 분할**(누적 길이 불변 → 자막 싱크 무영향). +5. **draft**: §5 빌더 호출. 좌표는 전부 `_template_pos()`가 레이아웃 상수에서 파생(§9). + +### 4-B. 📋 붙여넣기 (기본 탭) → `process_paste()` (pipeline.py) + +LLM이 만든 편집안 JSON을 **그대로** 사용. 무음컷·ASR **기본 없음**(옵션으로 무음 제거 가능). + +단계: `download(컷 정밀) → [remove_silence] → [scene] → draft` + +1. **파싱** (`paste.parse_paste`): 관대한 JSON 파싱 — + `json.loads(strict=False)`(자막 안 실제 줄바꿈 허용), 코드펜스(```) 자동 제거, + 시간은 `parse_time()`: `"분:초.밀리"`/`"시:분:초.밀리"`/초 단독, 콤마 밀리초도 허용. + 검증 실패 시 한국어 메시지로 400. +2. **download**: `download_paste_cuts()` — 컷마다 `cut_youtube_precise()`: + `--download-sections "*HH:MM:SS.mmm-…" --force-keyframes-at-cuts`(프레임 정확). + **실패 시 keyframe 컷으로 자동 폴백**(일부 영상에서 ffmpeg 크래시 방어). + 전부 받은 뒤 concat 재인코딩 병합. 제목/채널 자동 추출. +3. **자막 배치**: 배치 시간은 **컷 순서 누적 자동 계산** (LLM이 배치시간 계산하면 + 오타 나므로 입력받지 않음). `bottom` → 하단 자막, `effect` → 중앙 효과자막. +4. **remove_silence** (옵션, 기본 꺼짐): 병합본에서 무음 감지 → 컷 추가 압축, + 자막을 `_remap_caps()`로 압축 타임라인에 재매핑(무음에만 걸친 자막은 버림). +5. **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 매핑 필수. +6. **scene / 댓글카드 / draft**: 4-A와 동일(좌표도 공통 — §9). + +#### 붙여넣기 JSON 스키마 (LLM에게 시킬 형식) + +```json +{ + "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. 전체 길이 표시. +- **효과자막**: 녹색 `#0dff63` size13 **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/` +- ⚠ **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초, 영상 길이 초과분은 생략. +- 렌더: 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-8` env + stdout 파싱 대신 **glob으로 결과 파일 탐색** + (Windows cp949 디코드 깨짐 방어). 모든 subprocess는 `encoding="utf-8", errors="replace"`. +- 정밀 컷: `--force-keyframes-at-cuts` 1차 → 실패 시 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. +- "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`는 더 이상 레이아웃에 관여하지 않는다.) + +```python +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 저장으로 원복). +- `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를 재사용(네트워크 불필요). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c9eee8e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,84 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 이 프로젝트가 하는 일 + +유튜브 영상(URL/파일/LLM 편집안 JSON) → 다운로드·컷·자막·배경 템플릿을 자동 조립해 +**편집 가능한 CapCut 드래프트**(`draft_content.json`)를 로컬 CapCut 프로젝트 폴더에 생성하는 +FastAPI + 정적 HTML 로컬 웹앱. 포트 **8001** (형제 앱 v1 `../capcut`은 8000, 완전히 별개 코드베이스). + +## 먼저 읽어야 할 문서 + +이 저장소에는 이미 깊은 명세 문서가 있다. 코드를 건드리기 전에 반드시 참고: + +- **`ARCHITECTURE.md`** — 전체 구현 명세(좌표계, 파이프라인 단계별 동작, 드래프트 빌더 내부, + yt-dlp 함정, 고정값 치트시트, "하지 말 것" 목록). **가장 정확하고 깊은 문서.** +- **`README.md`** — 사용자용 요약, 붙여넣기 JSON 스키마, 다른 PC 설치법, 문제 해결표. + +CLAUDE.md는 위 두 문서와 중복하지 않는다. 세부는 그쪽을 볼 것. + +## 실행 / 개발 명령 + +```bash +# 서버 실행 (Windows 런처 — 브라우저 자동 오픈, 포트 8001) +캡컷_에이전트_구간합치기.bat + +# 런처 없이 직접 실행 +python -m uvicorn server.app:app --port 8001 + +# 의존성 설치 +python -m pip install -r requirements.txt + +# 파이썬 구문 검증 (수정 후 습관) +python -c "import ast; ast.parse(open('capcut_agent/pipeline.py', encoding='utf-8').read())" +python -c "from server import app" # 임포트 체인 확인 +``` + +시스템 의존성(pip 아님, PATH 필요): **ffmpeg/ffprobe**, **Node.js 또는 deno**(yt-dlp JS 런타임), +**CapCut**(드래프트 열기 + 코트라 볼드체 폰트 캐시). + +### ⚠️ 가장 중요한 규칙: 코드 수정 후 .bat 재시작 필수 + +파이썬 코드가 uvicorn 서버에 물려 있어 **hot-reload 안 됨**. 코드를 고쳤으면 반드시 +검은 창을 닫고 `.bat`을 다시 실행해야 반영된다. 이것을 잊는 게 "안 돼요"의 가장 흔한 원인. + +## 아키텍처 큰 그림 + +요청은 세 탭(파일 / 유튜브 구간 / 붙여넣기)에서 들어와 두 파이프라인 중 하나로 갈린다: + +- **`server/app.py`** — FastAPI. 엔드포인트 4개(`/upload` `/youtube` `/paste` `/open-capcut`) + + SSE 스트림(`/stream/{job_id}`). job은 메모리 dict `JOBS[hash]`에 저장(서버 재시작 시 소실). + 폼 필드 → 파이프라인 인자 변환 담당. +- **`server/static/index.html`** — UI 전체(단일 파일, 탭 3개 + 옵션 + SSE 렌더). +- **`capcut_agent/pipeline.py`** — ★ 진입점 두 개: + - `process_bg_template()` — 파일/유튜브 탭. `download → silence → asr → [scene] → draft` + - `process_paste()` — 붙여넣기 탭. `download(정밀 컷) → [remove_silence] → [asr_bottom] → [scene] → draft` +- **`capcut_agent/draft.py`** — ★ `build_bg_template_draft`. pycapcut으로 드래프트 생성 후 + `draft_content.json`을 직접 후처리(폰트·그림자 주입 등). +- 나머지 `capcut_agent/*.py` — youtube(다운로드/병합), paste(관대한 JSON 파서), silence, + transcribe(faster-whisper), correct(Gemini 교정), scene, media, probe. 역할은 ARCHITECTURE.md §1. + +### 절대 되돌리면 안 되는 핵심 설계 결정 + +- **자막 타이밍 = Whisper 단어 타임스탬프, 글자만 = Gemini 제자리 교정(시간 불변).** + 과거에 Gemini 오디오 전사 타임스탬프를 직접 쓰거나 글자수 기반 정렬을 시도했다가 + 누적 드리프트로 자막이 밀렸다. Gemini는 글자만 1:1 교정, 줄 수·순서·시간은 절대 건드리지 않는다. +- **붙여넣기 탭은 배치 시간을 입력받지 않는다** — 컷 순서대로 누적 자동 계산(LLM이 계산하면 오타). +- **같은 텍스트 트랙에 동시간 세그먼트 2개 금지** — CapCut `SegmentOverlap` 에러. +- 정답 파일은 **`draft_content.json`** (draft_info.json 아님). + +### 좌표계 (draft.py 작업 시) + +`CapCut Y = transform_y × 1920` (canvas 1080×1920, 위가 +/아래가 −). 헬퍼 `_ty(y_px) = (960 - y_px)/960`. +확대(%) = scale 비율. 사용자가 "위치 −559로" 라고 하면 transform_y로 환산해 반영. 고정값은 ARCHITECTURE.md §9 치트시트. + +## 검증 방식 + +이 프로젝트에 자동 테스트 스위트는 없다. 검증은: +1. 구문 파싱 + 임포트 체인 확인(위 명령). +2. 가능하면 실제 드래프트를 빌드해 `draft_content.json` 값을 열어 확인(테스트 드래프트는 확인 후 삭제). +3. 최종적으로 **사용자가 CapCut에서 열어 확인**. + +테스트용 영상이 필요하면 `.downloads/`의 기존 mp4 재사용(네트워크 불필요). +Windows 콘솔은 cp949라 한글/특수문자 print가 깨져 보일 수 있음(로직과 무관). diff --git a/README.md b/README.md new file mode 100644 index 0000000..6adb131 --- /dev/null +++ b/README.md @@ -0,0 +1,139 @@ +# 캡컷 에이전트 · 구간합치기 (v2) + +유튜브 영상 → **무음컷 · 자막 · 배경 템플릿**이 박힌 **편집 가능한 CapCut 드래프트**를 자동 생성하는 로컬 웹앱. +특히 **한 URL의 여러 구간을 이어붙이고**, LLM이 만든 편집안(컷+자막)을 **JSON 붙여넣기**로 한 번에 처리합니다. + +--- + +## 1. 처음 실행 (이 PC) + +1. `캡컷_에이전트_구간합치기.bat` 더블클릭 +2. 잠시 후 브라우저가 http://127.0.0.1:8001 로 자동으로 열림 +3. **이 검은 창은 켜두세요** (닫으면 서버가 꺼짐) + +> 코드를 수정했으면 **반드시 창을 닫고 .bat 을 다시 실행**해야 반영됩니다. + +--- + +## 2. 세 가지 입력 방법 (탭) + +### 📋 붙여넣기 (기본 탭) — 추천 +LLM이 만든 편집안 JSON을 붙여넣으면 컷·자막을 **그대로** 사용합니다. (무음컷·받아쓰기 없음) + +### ▶ 유튜브 구간 +한 URL + 여러 구간(+ 구간 추가) → 이어붙여 **무음컷 + 자동 자막(Whisper)**. + +### 📁 파일 +로컬 영상 파일 → **무음컷 + 자동 자막**. + +세 방법 모두 아래 **영상 옵션**을 함께 적용합니다. + +--- + +## 3. 붙여넣기 JSON 형식 (핵심) + +아래 **JSON 하나**만 붙여넣으면 됩니다. 배치 시간·SRT 타임코드는 **넣지 마세요** — 앱이 컷을 순서대로 이어붙이며 자동 계산합니다. + +```json +{ + "url": "https://www.youtube.com/watch?v=영상ID", + "title_top": "서브제목", + "title_main": "메인제목", + "channel": "@채널명", + "cuts": [ + {"start":"0:01.0","end":"0:03.5","bottom":"하단 자막 윗줄\n하단 자막 아랫줄","effect":"효과자막"}, + {"start":"2:33.5","end":"2:36.5","bottom":"다음 컷 자막","effect":"공포의통계"} + ] +} +``` + +| 필드 | 설명 | +|------|------| +| `url` | **실제 영상 주소** (브라우저에서 열리는 것). LLM이 지어낸 가짜 ID 금지 | +| `cuts[].start` / `end` | 원본 영상 타임스탬프. `분:초.밀리` 또는 `시:분:초.밀리` (밀리초 생략 가능) | +| `cuts[].bottom` | 하단 자막. `\n` 이 있으면 **같은 자리에서 시간을 반씩 나눠** 윗줄→아랫줄 순서로 표시 | +| `cuts[].effect` | 중앙 효과 자막(녹색). 짧게 | +| `title_top` / `title_main` / `channel` | 선택. 비우면 안 들어감(채널은 유튜브에서 자동) | + +### LLM 에게 요청할 때 (이대로 복사) + +> 아래 스키마의 **JSON 하나로만** 출력해. 설명·마크다운 금지. +> - `url` 은 내가 준 이 주소를 **그대로** 써라(임의 생성 금지): `여기에_실제_URL` +> - `start`/`end` 는 원본 영상 타임스탬프(`분:초.밀리`). +> - **배치 시간은 계산하지 마라.** 앱이 순서대로 이어붙인다. +> - `bottom` 은 2줄(`\n`), `effect` 는 짧은 한 마디. +> - 모든 컷은 `start < end`. + +> 💡 자막이 안 쪼개지고 `\n` 글자가 그대로 보여도 앱이 알아서 처리합니다(`\n`·`\\n` 모두 인식). + +--- + +## 4. 영상 옵션 (공통) + +- **영상 확대** — 기본 144%. 슬라이더로 조절(캡컷에서 다시 조정 가능) +- **좌우반전(미러)** — 영상만 좌우 뒤집기 +- **장면분할** — 화면이 확 바뀌는 지점마다 컷 자동 분할(캡컷에서 개별 편집 가능). 시간이 더 걸림 +- **완료되면 CapCut 자동 실행** — 체크 시 결과 후 캡컷 자동 오픈 + +자막 위치·색은 코드 기본값(하단자막 CapCut Y=-559, 효과자막 Y=866·녹색 `#0dff63`). + +--- + +## 5. 다른 PC 에서 실행 + +이 `capcut2` 폴더를 통째로 복사한 뒤(`.downloads`·`.cache`·`.uploads` 캐시는 빼도 됨 — 자동 생성): + +1. **Python 3.10+** 설치 — https://python.org (설치 시 **Add to PATH** 체크) +2. 이 폴더에서 패키지 설치: + ``` + python -m pip install -r requirements.txt + ``` +3. **ffmpeg / ffprobe** 설치(PATH 필요) — `winget install Gyan.FFmpeg` +4. **Node.js** 설치 — https://nodejs.org (yt-dlp 유튜브 추출용 JS 런타임) +5. **CapCut** 설치 +6. `캡컷_에이전트_구간합치기.bat` 실행 + +> **코트라 볼드체**: 새 PC의 CapCut 에서 그 폰트를 한 번 사용하면 캐시가 생겨 자동 적용됩니다. 없으면 기본 폰트로 나옵니다(에러 아님). +> **드래프트 저장 위치**: 그 PC의 CapCut 프로젝트 폴더를 자동 인식합니다. +> **.gemini_key**: '파일'·'유튜브 구간' 탭 자막 교정용. 붙여넣기 탭만 쓰면 없어도 됩니다. + +--- + +## 6. 문제 해결 + +| 증상 | 해결 | +|------|------| +| 수정한 게 반영 안 됨 | 검은 창 닫고 **.bat 재시작** | +| `python not found` | Python 재설치 시 PATH 체크 후 재부팅 | +| 유튜브 `Video unavailable` | 영상이 실제 공개인지 확인 → `python -m pip install -U yt-dlp` | +| 다운로드가 자꾸 깨짐 | `python -m pip install -U yt-dlp` (유튜브가 가끔 바뀜) | +| 자막이 밀림 | 붙여넣기 탭은 컷·자막을 그대로 쓰므로 안 밀림. 파일/유튜브 탭은 Whisper 타이밍 사용 | +| 자막 `\n` 이 글자로 박힘 | .bat 재시작하면 해결(앱이 `\n`·`\\n` 자동 분할) | +| 폰트가 다르게 나옴 | CapCut 에서 코트라 볼드체 한 번 사용해 캐시 생성 | + +--- + +## 7. 폴더 구조 (참고) + +``` +capcut2/ +├─ 캡컷_에이전트_구간합치기.bat 실행 파일(포트 8001) +├─ 배경.png 배경 템플릿(검정-흰-검정) +├─ requirements.txt 파이썬 패키지 목록 +├─ .gemini_key (선택) Gemini 키 +├─ server/ +│ ├─ app.py FastAPI 서버 (/upload /youtube /paste /stream) +│ └─ static/index.html 웹 UI +└─ capcut_agent/ + ├─ pipeline.py 처리 파이프라인(다운로드→컷→자막→드래프트) + ├─ youtube.py 유튜브 구간/다중/정밀 다운로드·병합 + ├─ paste.py 붙여넣기 JSON 파서 + ├─ draft.py CapCut 드래프트 생성(pycapcut) + ├─ scene.py 장면전환 감지·분할 + ├─ silence.py 무음 감지 + ├─ transcribe.py Whisper 받아쓰기(파일/유튜브 탭) + ├─ correct.py Gemini 자막 교정(선택) + ├─ highlight.py 자막 청킹 + ├─ media.py 프레임/오디오 처리 + └─ probe.py 영상 메타 조회 +``` diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..eea49b3 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,316 @@ +# SETUP.md — 다른 PC 설치 · 환경 · 버전 명세 + +> 이 프로젝트(캡컷 에이전트 · 구간합치기 v2)를 **다른 Windows PC에서 그대로 돌리기 위한** 전체 스펙·버전 문서. +> 구현 세부는 [ARCHITECTURE.md](ARCHITECTURE.md), 사용법은 [README.md](README.md) 참고. +> 아래 버전들은 **현재 작동 중인 PC에서 실측한 값**(2026-07 기준)이라, 이 조합이면 확실히 돕니다. + +--- + +## 0. 30초 요약 체크리스트 + +다른 PC에서 이 6개만 맞추면 됩니다: + +- [ ] **Windows 10/11** (64-bit) +- [ ] **Python 3.13** 설치 + PATH 등록 +- [ ] **ffmpeg / ffprobe** PATH 등록 (pip 아님) +- [ ] **Node.js**(또는 deno) PATH 등록 (yt-dlp JS 런타임) +- [ ] **CapCut** 설치 + (선택) 코트라 볼드체 1회 사용해 폰트 캐시 생성 +- [ ] `python -m pip install -r requirements.txt` 실행 + +그다음 `캡컷_에이전트_구간합치기.bat` 더블클릭 → http://127.0.0.1:8001 + +--- + +## 1. 시스템 요구사항 + +| 항목 | 실측 버전 | 비고 | +|---|---|---| +| OS | Windows 11 (10.0.26200) | Windows 10 64-bit 이상 권장. 콘솔 cp949라 한글 print 깨져 보여도 로직 무관 | +| Python | **3.13.0** | 3.10~3.13 범위면 대체로 OK. python.org 설치 시 "Add to PATH" 체크 | +| 아키텍처 | x64 | faster-whisper(ctranslate2)·onnxruntime가 x64 전제 | +| 디스크 여유 | 약 3~4GB | Whisper medium 모델(~1.5GB) + 패키지 + 다운로드 캐시 | +| 인터넷 | 최초 1회 필수 | 패키지·Whisper 모델·유튜브 다운로드에 필요 | + +### 권장 사양 (쾌적하게 돌리려면) + +병목은 **자막 받아쓰기(ASR)** 입니다. faster-whisper `medium` 모델을 **CPU(int8)** 로 돌리므로 +**CPU 성능·코어 수가 속도를 좌우**합니다. (아래 GPU 항목 참고 — 현재 GPU는 안 씀.) + +| 항목 | 최소 | 권장 | 비고 | +|---|---|---|---| +| **CPU** | 4코어 | **8코어 이상** (최신 Ryzen 5/7, Intel i5/i7) | ASR·ffmpeg 재인코딩이 CPU 바운드. 코어 많을수록 자막·병합 빠름 | +| **RAM** | 8GB | **16GB** | Whisper 모델 로드(~2~3GB) + ffmpeg 재인코딩 + 브라우저 동시 | +| **저장소** | HDD 가능 | **SSD(NVMe 권장)** | 영상 재인코딩·프레임 추출 I/O가 많음. SSD면 체감 큰 차이 | +| **디스크 여유** | 4GB | **10GB+** | 여러 영상 다운로드·ASR 캐시가 쌓임(`.downloads` `.cache`) | +| **GPU** | 불필요 | 불필요 | ⚠ **현재 코드는 Whisper를 `device="cpu"` 로 고정** → GPU 있어도 이득 없음. CPU에 투자할 것 | +| **네트워크** | — | 안정적 유선/와이파이 | 유튜브 다운로드용. 구간 컷 속도는 회선보다 **ffmpeg 8.0.1**(§2-1)이 핵심 | + +> **속도 감각**: 붙여넣기 탭에서 ASR을 끄면 CPU 부담이 확 줄어 어떤 PC에서도 빠릅니다. +> 파일/유튜브 탭(자동 자막)은 CPU가 약하면 영상 길이에 비례해 ASR이 오래 걸립니다. + +--- + +## 2. 시스템 의존성 (pip 아님 — 별도 설치 + PATH 필수) + +이 3개는 파이썬 패키지가 아니라 **OS에 따로 깔고 PATH에 잡혀야** 합니다. 없으면 조용히 실패하거나 ffmpeg 크래시가 납니다. + +### 2-1. ffmpeg / ffprobe ★필수 · ⭐버전 8.0.1 반드시 고정 + +| 항목 | 실측 | +|---|---| +| 버전 | **ffmpeg 8.0.1** (gyan.dev `essentials_build`) — ⭐**이 버전으로 고정** | +| 확인 | `ffmpeg -version`, `ffprobe -version` 둘 다 나와야 함 | + +> ⚠️ **가장 중요 — 최신(8.1.x) 쓰지 말 것.** ffmpeg **8.1.x**는 `--download-sections`(유튜브 구간 컷) +> 경로에서 **HTTP seek 회귀**가 있어 10초 클립 다운로드가 **90초+** 로 극단적으로 느려집니다. +> **8.0.1로 내리면 12~15초로 정상화.** (전체 다운로드는 이 경로를 안 타서 멀쩡하므로, +> "일반 다운로드는 빠른데 구간 컷만 느리다"면 100% 이 문제입니다.) + +- 다운로드(8.0.1 정확히): **https://github.com/GyanD/codexffmpeg/releases/tag/8.0.1** → `ffmpeg-8.0.1-essentials_build.7z` +- 압축 해제 후 `bin` 폴더(= `ffmpeg.exe`, `ffprobe.exe`)를 **시스템 PATH에 추가**. 이미 8.1.x가 잡혀 있으면 **그 경로를 지우고** 8.0.1로 교체(또는 exe 2개 덮어쓰기). +- **터미널 새로 열고** `ffmpeg -version`이 `8.0.1-essentials_build`인지 확인. +- 용도: 구간 병합·재인코딩, 무음 감지, 장면분할, 오디오 추출, 프레임 추출. + +**속도 검증**(셋업 후 10초 구간 컷이 몇 초 걸리는지): +```powershell +yt-dlp --js-runtimes node --download-sections "*0:30-0:40" --force-keyframes-at-cuts -f "bv*[vcodec^=avc1]+ba[acodec^=mp4a]/b" --merge-output-format mp4 -o "%TEMP%/sec.%(ext)s" "https://www.youtube.com/watch?v=dQw4w9WgXcQ" +``` +→ **12~15초 = ✅ 정상** / 90초+ = ffmpeg가 아직 8.1.x (터미널 새로 열었는지 재확인). + +### 2-2. JS 런타임 (Node.js 또는 deno) ★필수 + +| 항목 | 실측 | +|---|---| +| Node.js | **v22.17.0** | +| deno | 1.3.14 (있으면 우선 사용) | + +- 최신 유튜브는 JS 챌린지가 있어 **런타임이 없으면 포맷 누락 → ffmpeg 크래시**가 납니다. +- `youtube.py`가 `deno → node → bun` 순으로 자동 감지(`--js-runtimes`)하므로 **셋 중 하나만** 있으면 됨. +- 가장 쉬운 선택: **Node.js LTS** 설치 (https://nodejs.org) → `node --version` 확인. + +### 2-3. CapCut ★필수 + +| 용도 | 필수 여부 | +|---|---| +| 드래프트 저장 위치 제공(`%LOCALAPPDATA%/CapCut/...`) | 필수 | +| 완료 후 자동 열기(`/open-capcut`) | 선택 | +| 코트라 볼드체 폰트 캐시 | 선택(없으면 기본 폰트로 안전 동작) | + +- CapCut 데스크톱(Windows) 설치. 드래프트는 아래 경로에 생성됨: + `%LOCALAPPDATA%\CapCut\User Data\Projects\com.lveditor.draft\<드래프트명>\` +- 폰트 캐시는 §6 참고. + +--- + +## 3. Python 패키지 (pip) + +### 3-1. 느슨한 설치 (requirements.txt — 최신으로 받음) + +```bash +python -m pip install -r requirements.txt +``` + +`requirements.txt` 내용: +``` +fastapi +uvicorn +python-multipart +pyCapCut +Pillow +pymediainfo +yt-dlp +faster-whisper +``` + +### 3-2. 버전 고정 (재현성 100% — 이 조합이 실제로 도는 버전) + +다른 PC에서 **최신 버전 충돌이 걱정되면** 아래를 `requirements.lock.txt`로 저장해 +`python -m pip install -r requirements.lock.txt` 로 설치하세요. + +``` +# ── 직접 의존성 ────────────────────────────── +fastapi==0.115.14 +uvicorn==0.35.0 +python-multipart==0.0.20 +pyCapCut==0.0.3 # import 이름은 pycapcut +Pillow==10.4.0 +pymediainfo==7.0.1 +yt-dlp==2026.7.4 +faster-whisper==1.2.1 + +# ── faster-whisper / fastapi 전이 의존성(자동 설치되지만 버전 고정용) ── +av==16.0.1 +ctranslate2==4.6.2 +onnxruntime==1.23.2 +tokenizers==0.21.2 +huggingface-hub==0.33.4 +numpy==2.2.0 +starlette==0.46.2 +pydantic==2.11.7 +``` + +> **참고** +> - `yt-dlp`는 pip로 깔면 `yt-dlp` **콘솔 스크립트가 PATH에 생겨** 코드가 subprocess로 호출합니다. (유튜브는 자주 막히니 **주기적으로 `python -m pip install -U yt-dlp` 업데이트 권장** — 버전 고정하지 말 것.) +> - SSE(진행상황 스트림)는 FastAPI 내장 `StreamingResponse` 사용 → `sse-starlette` **불필요**. +> - Gemini 교정은 표준 라이브러리 `urllib`로 REST 직접 호출 → `google-generativeai` **패키지 불필요**. +> - `pymediainfo`는 내부적으로 **MediaInfo DLL**을 씀. 대개 wheel에 포함되나, 안 되면 https://mediaarea.net/en/MediaInfo 의 DLL을 PATH에 두면 됨. + +--- + +## 4. 자동 다운로드되는 모델 (faster-whisper) + +파일/유튜브 탭에서 **자막 받아쓰기(ASR)** 를 처음 실행할 때, HuggingFace에서 자동 다운로드됩니다. + +| 항목 | 값 | +|---|---| +| 모델 | `Systran/faster-whisper-medium` | +| 크기 | 약 **1.5GB** | +| 설정 | `medium` / `compute_type=int8` / `device=cpu` / 언어 `ko` | +| 캐시 위치 | `%USERPROFILE%\.cache\huggingface\hub\models--Systran--faster-whisper-medium` | +| ASR 결과 캐시 | 프로젝트 `.cache\` (content-hash 기준) | + +- **최초 1회 인터넷 필요.** 이후 오프라인 동작. +- **붙여넣기 탭만 쓰고 ASR(하단자막 자동생성)을 끄면** 이 모델은 안 받아도 됨. +- 미리 받아두려면 아무 영상이나 파일 탭에 한 번 돌리면 캐시됨. (또는 다른 PC의 위 캐시 폴더를 통째로 복사해도 됨.) + +--- + +## 5. (선택) Gemini 자막 글자 교정 + +자막 **글자만** 1:1 교정(시간 불변). 키 없으면 **자동 스킵**(Whisper 원문 유지)이라 필수는 아님. + +| 항목 | 값 | +|---|---| +| 모델 | `gemini-2.5-flash` (무료 티어 지원) | +| 호출 | REST(`generativelanguage.googleapis.com`) via `urllib` — 추가 패키지 없음 | +| 키 주입 방법 (둘 중 하나) | ① 프로젝트 루트에 **`.gemini_key`** 파일(키 한 줄) ② 환경변수 `GEMINI_API_KEY` 또는 `GOOGLE_API_KEY` | + +- 키 발급: https://aistudio.google.com → API key +- 429(무료 한도 초과) 시 Whisper 원문으로 폴백. + +--- + +## 6. 코트라 볼드체 폰트 (선택, 있으면 예쁨) + +pycapcut FontType에 없어 **저장 후 draft_content.json에 폰트 경로를 직접 주입**합니다. + +| 항목 | 값 | +|---|---| +| 캐시 경로 | `%LOCALAPPDATA%\CapCut\User Data\Cache\effect\7480846567709265157\782a91b14f1661b95e7e587be27f1af4\font.ttf` | +| 크기 | 약 642KB | +| 경로 성격 | **PC 무관**(CapCut 전역 폰트 ID라 어느 PC든 동일 경로) | + +- 이 파일이 **있어야** 자막/제목이 코트라 볼드체로 나옴. 없으면 주입을 **자동 생략** → CapCut 기본 폰트로 안전 동작(에러 아님). +- 다른 PC에서 만들려면: 그 PC의 **CapCut에서 코트라 볼드체를 한 번 사용**(아무 텍스트에 적용)하면 캐시가 생성됨. 또는 위 `font.ttf`를 같은 경로에 복사. + +--- + +## 7. 다른 PC 설치 순서 (처음부터) + +```powershell +# 1) Python 3.13 설치 (python.org, "Add Python to PATH" 체크) → 확인 +python --version + +# 2) ffmpeg 설치 후 bin 폴더를 PATH 등록 → 확인 +ffmpeg -version +ffprobe -version + +# 3) Node.js LTS 설치 → 확인 +node --version + +# 4) CapCut 설치 (그리고 원하면 코트라 볼드체 1회 사용) + +# 5) 프로젝트 폴더 통째로 복사한 뒤, 그 폴더에서: +python -m pip install -r requirements.txt + +# 6) (선택) Gemini 키 +# .gemini_key 파일에 키 한 줄 저장 또는 환경변수 GEMINI_API_KEY 설정 + +# 7) 실행 +캡컷_에이전트_구간합치기.bat +``` + +> 폴더를 복사할 때 `.cache/`, `.downloads/` 같은 대용량 파생물은 빼도 됨(자동 재생성). +> `.gemini_key`는 개인 키라 공유 주의. + +--- + +## 8. 실행 · 포트 + +| 항목 | 값 | +|---|---| +| 런처 | `캡컷_에이전트_구간합치기.bat` (브라우저 자동 오픈) | +| 직접 실행 | `python -m uvicorn server.app:app --port 8001` | +| 주소 | http://127.0.0.1:8001 | +| 포트 | **8001** (형제 앱 v1 `../capcut`은 8000 — 동시 실행 가능) | + +> ⚠️ **코드 수정 후엔 반드시 검은 창 닫고 .bat 재실행.** uvicorn hot-reload 안 됨(가장 흔한 "안 돼요" 원인). + +--- + +## 9. 설치 검증 + +```bash +# 파이썬 임포트 체인 확인 (에러 없이 통과해야 함) +python -c "from server import app; print('app OK')" +python -c "from capcut_agent import pipeline, draft, youtube, transcribe, correct; print('modules OK')" + +# 외부 도구 확인 +ffmpeg -version | head -1 +node --version +yt-dlp --version +``` + +최종 검증은 실제로 짧은 유튜브 구간 하나를 돌려 **CapCut에서 드래프트가 열리는지** 확인. + +--- + +## 10. 문제 해결 (다른 PC 이식 시 흔한 것) + +| 증상 | 원인 | 해결 | +|---|---|---| +| `ffmpeg`/`ffprobe` not found | PATH 미등록 | ffmpeg `bin`을 시스템 PATH에 추가, 창 새로 열기 | +| 유튜브 다운로드가 ffmpeg 크래시(exit 3436169992) | JS 런타임 없음 | Node.js 또는 deno 설치 | +| "Video unavailable" | 영상 자체 없음/지역제한 | `curl "https://www.youtube.com/oembed?url=&format=json"` 404면 영상 문제 | +| 유튜브가 갑자기 다 실패 | yt-dlp 구버전 | `python -m pip install -U yt-dlp` | +| 자막이 기본 폰트로 나옴 | 코트라 볼드체 캐시 없음 | §6 — CapCut에서 1회 사용 or font.ttf 복사 (없어도 동작은 함) | +| ASR 첫 실행이 매우 느림/멈춘 듯 | Whisper medium(1.5GB) 다운로드 중 | 최초 1회. 인터넷 확인, 기다리기 | +| 자막 교정이 안 됨 | Gemini 키 없음/429 | 선택 기능. 없으면 Whisper 원문 사용(정상) | +| 병합 시 `Invalid argument`(exit 4294967274) | (해결됨) 한글 경로 concat 버그 | 최신 `youtube.py`면 ASCII 임시링크로 자동 우회 | +| 드래프트 열면 영상이 흰띠·댓글 위로 삐짐 | CapCut 편집 중 render_index 꼬임 | 최신 `draft.py`는 오버레이/제목 트랙 자동 잠금으로 예방 | +| **클립을 옮긴 뒤 확대하면 그 클립만 템플릿 밖으로 삐짐** | CapCut이 옮긴 클립에 렌더순서를 새로(맨 위로) 매김 — 잠금으로 못 막음 | **캡컷에서 그 프로젝트를 닫고** → 웹 UI 하단 **"🩹 레이어 수리"** → 드래프트 선택 → 실행 → 캡컷에서 다시 열기 | +| **영상 중간에 초록 화면이 몇 초 나옴** | yt-dlp가 키프레임 아닌 위치에서 잘라 참조 프레임이 없음 | 최신 `youtube.py`가 컷마다 자동 검증→재다운로드→정밀 재컷. 로그에 `🩹` 표시. 예전에 받은 영상은 다시 만들어야 함 | +| 콘솔에 한글 깨짐 | Windows cp949 | 표시만 깨짐. 로직·결과와 무관 | + +--- + +## 11. 한눈에 보는 버전 표 (복붙용) + +``` +OS Windows 11 (10.0.26200), x64 +Python 3.13.0 +ffmpeg/ffprobe 8.0.1 (gyan.dev essentials) +Node.js 22.17.0 (또는 deno 1.3.14) +CapCut 데스크톱(Windows) + +fastapi 0.115.14 +uvicorn 0.35.0 +python-multipart 0.0.20 +pyCapCut 0.0.3 (import pycapcut) +Pillow 10.4.0 +pymediainfo 7.0.1 +yt-dlp 2026.7.4 (최신 유지 권장) +faster-whisper 1.2.1 + ├ av 16.0.1 + ├ ctranslate2 4.6.2 + ├ onnxruntime 1.23.2 + ├ tokenizers 0.21.2 + ├ huggingface-hub 0.33.4 + └ numpy 2.2.0 +starlette 0.46.2 +pydantic 2.11.7 + +ASR 모델 Systran/faster-whisper-medium (~1.5GB, int8/cpu) +Gemini(선택) gemini-2.5-flash (REST/urllib, 키 선택) +포트 8001 +``` diff --git a/assets/bg_template.png b/assets/bg_template.png new file mode 100644 index 0000000..5fa6fbf Binary files /dev/null and b/assets/bg_template.png differ diff --git a/assets/bg_white.png b/assets/bg_white.png new file mode 100644 index 0000000..41c65f1 Binary files /dev/null and b/assets/bg_white.png differ diff --git a/assets/frame_template.png b/assets/frame_template.png new file mode 100644 index 0000000..1134670 Binary files /dev/null and b/assets/frame_template.png differ diff --git a/assets/frame_template_white.png b/assets/frame_template_white.png new file mode 100644 index 0000000..38bb28e Binary files /dev/null and b/assets/frame_template_white.png differ diff --git a/build_bg_template.py b/build_bg_template.py new file mode 100644 index 0000000..8c2d514 --- /dev/null +++ b/build_bg_template.py @@ -0,0 +1,92 @@ +"""배경템플릿 CLI: 잘라둔 클립 → 무음컷 + 자막 + (검정-흰색-검정) 배경 템플릿. + +영상은 흰 영역 위 '움직일 수 있는' 레이어 → 캡컷에서 확대/좌우 이동은 사용자가. +제목(2줄)·자막·채널은 캡컷 편집 가능 텍스트. + +사용: + python build_bg_template.py