capcut-agent/docs/superpowers/specs/2026-07-31-자동탭-오팔대체-댓글자동매칭-design.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

340 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 자동 탭 — 오팔 대체 + 댓글 카드 자동 매칭 (설계)
작성일: 2026-07-31
## 1. 배경과 목적
지금 숏폼 5개를 만들려면 사람이 이 순서로 움직인다:
1. 오팔(opal.google.com) 편집기를 열어 Step 3 노드 5개를 **하나씩** 클릭해 JSON을 복사
2. capcut2 붙여넣기 탭에 붙여넣고 실행 → 끝날 때까지 기다림 → 다음 것 복사 (5회 반복)
3. h-lab 댓글 카드 페이지를 열어 영상 댓글을 가져오고, 카드를 골라 PNG로 저장한 뒤
`댓글카드/` 폴더에 넣어둠
복사·붙여넣기 5회 + 브라우저 왕복 + 댓글 수작업이 전부 손이다. 이걸 **유튜브 URL 하나 →
검토 화면에서 클릭 몇 번 → 드래프트 5개**로 줄인다.
핵심 착안점: 오팔 Step 3의 결과에는 `start_time`/`end_time`이 있고, 유튜브 댓글 상당수는
본문에 `2:14` 같은 타임스탬프를 적는다. **그 구간을 언급한 댓글**을 자동으로 뽑으면
"영상에 맞는 댓글 카드"가 사람 손 없이 정해진다.
## 2. 범위
### 하는 것
- 새 탭 **🤖 자동** 추가 — 유튜브 URL 입력 → Gemini 분석 → 검토 → 드래프트 5개 생성
- Gemini API로 오팔 Step 1/Step 3 대체 (`.gemini_key` 재사용)
- h-lab 원격 API에서 댓글을 받아 구간별 자동 매칭 + 부족분 수동 선택
- 브라우저에서 댓글 카드 PNG를 구워 서버로 업로드 (h-lab 카드 디자인 그대로)
- 프롬프트를 파일로 분리해 UI/메모장 어느 쪽에서든 수정 가능
- `_load_comment_cards` 배치 규칙 변경 (모자라면 균등 분배) — **기존 3개 탭에도 적용**
### 하지 않는 것 (명시)
- **붙여넣기 탭을 없애지 않는다.** 오팔을 계속 써도 되고, Gemini가 실패하면 그리로 도망갈 수 있어야 한다.
- **h-lab 코드를 고치지 않는다.** 필요한 API가 이미 다 열려 있다 (§7에 근거).
- **서버에 작업 큐를 만들지 않는다.** 브라우저가 `/auto/build`를 하나씩 순차 호출하면 충분하다.
- **Pillow로 카드를 그리지 않는다.** 디자인이 h-lab과 달라진다.
- **빌드를 병렬로 돌리지 않는다.** yt-dlp·ffmpeg·Whisper가 CPU를 다 쓴다.
- 오팔 Step 2를 옮기지 않는다 — 노드 간 값 전달용이라 코드에선 배열 인덱싱이다.
## 3. 전체 흐름
```
[유튜브 URL] → 분석 시작
├─ Gemini Step 1 (전체 영상) → 후보 5개 {id, start_time, end_time, reason}
├─ Gemini Step 3 × 5 (구간별, 동시) → 블록① JSON + 블록② 타이틀 후보 5선
└─ h-lab fetch × 1 → 댓글 전체 + 본문 mm:ss 파싱
[검토 화면] 하이라이트 카드 5장, 각각:
├ 제목 ▾ 타이틀 후보 5선에서 교체 가능
├ 컷 8개 · 총 52.3초 → 카드 17장 필요
├ ⭐ 2:14~4:02 구간을 언급한 댓글 9장 (좋아요순 자동 체크)
좋아요 상위 후보 20장 (부족한 8장을 클릭)
[5개 전부 만들기]
브라우저: 선택된 카드를 modern-screenshot으로 PNG 캡처(4배)
→ POST /auto/build (하이라이트 1개 + 카드 PNG들) → job_id
→ GET /stream/{job_id} 진행 표시
→ 끝나면 다음 하이라이트로 (순차 5회)
```
Gemini 호출은 총 6번. Step 3 5개는 **동시 요청**(약 1분 → 약 20초). 429(무료 한도)가 나면
그 구간만 순차로 재시도한다.
## 4. 구성요소
| 파일 | 상태 | 하는 일 |
|---|---|---|
| `capcut_agent/plan.py` | 신규 | Gemini 호출(Step 1 / Step 3), 응답에서 JSON 블록·타이틀 후보 추출 |
| `capcut_agent/comments.py` | 신규 | h-lab 댓글 fetch, 본문 타임스탬프 파싱, 구간 매칭·순위 |
| `capcut_agent/prompts.py` | 신규 | 프롬프트 파일 읽기/쓰기/기본값 복원 |
| `server/static/auto.js` | 신규 | 검토 화면, 카드 렌더·캡처, 순차 빌드 |
| `server/static/modern-screenshot.js` | 신규(벤더링) | DOM→PNG 캡처. CDN 대신 동봉해 오프라인·버전고정 |
| `server/app.py` | 수정 | 엔드포인트 5개 추가 |
| `server/static/index.html` | 수정 | 탭 1개 + 패널 추가, 카드 CSS 이식. 기존 3탭 손대지 않음 |
| `capcut_agent/pipeline.py` | 수정 | `_load_comment_cards` 배치 규칙 (§6) |
| `숏폼_편집_지침서_v13.7_capcut2연동판.md` | 기존 그대로 | **Step 3 프롬프트 설정 파일로 그대로 사용** |
| `프롬프트/하이라이트_선정.md` | 신규(자동 생성) | Step 1 프롬프트 |
### 4.1 `capcut_agent/plan.py`
```python
DEFAULT_MODEL = "gemini-3.5-flash" # 설정에서 교체 가능 (§4.5)
# correct.py 의 _gemini_key() / GeminiQuotaError 는 재사용
def select_highlights(url, *, key=None, prompt=None, model=None) -> list[dict]
# → [{"id":1, "start":134.0, "end":242.0, "reason":"…"}, …]
def edit_plan(url, start_sec, end_sec, *, key=None, prompt=None, model=None) -> dict
# → {"paste": {…parse_paste 결과…}, "titles": [{"top":…,"main":…,"kind":"어그로형"}, …]}
```
유튜브 URL을 Gemini에 그대로 넘기고, 구간은 `videoMetadata`로 자른다 (오팔 Step 2의 역할):
```json
{"contents":[{"parts":[
{"fileData":{"fileUri":"https://www.youtube.com/watch?v=…"},
"videoMetadata":{"startOffset":"134s","endOffset":"242s"}},
{"text":"<프롬프트 전문>"}]}],
"generationConfig":{"temperature":0.7}}
```
#### 영상 샘플링 — Step 1과 Step 3을 다르게 보낸다
Gemini는 영상을 기본 **1 FPS · 초당 약 300토큰**으로 읽는다. 그대로 두면 **Step 1(전체 영상)이
55분쯤에서 컨텍스트 100만 토큰을 넘겨 실패한다.** Step 3은 구간이 1분30초~3분이라 무관하다.
| | 보내는 것 | fps | 1시간 원본 기준 |
|---|---|---|---|
| Step 1 | 전체 영상 1회 | **0.2** (5초당 1프레임) | 약 22만 토큰 — 통과 |
| Step 3 | 구간 1개 × 5회 | 기본(1.0) | 구간당 약 5만 토큰 |
`videoMetadata.fps`로 지정한다. 하이라이트 구간을 고르는 일은 표정 디테일보다 흐름·오디오를
보는 작업이라 0.2 fps로 충분하고, **오디오는 fps와 무관하게 그대로 들어간다.** 정밀한 컷 지점과
verbatim 자막이 필요한 Step 3만 기본 fps로 보낸다.
Step 1의 fps도 설정값이다 — 짧은 영상만 다루게 되면 올리면 된다.
응답 파싱:
- Step 3은 블록 ①②③ 세 덩어리로 오므로 **첫 번째 ` ```json ` 펜스 안쪽**만 꺼낸다.
펜스가 없으면 본문 전체를 시도한다.
- 꺼낸 JSON은 **기존 `parse_paste()`에 그대로 통과시킨다** — 검증 로직을 두 벌 만들지 않는다.
- `url` 필드는 파싱 후 **사용자가 입력한 URL로 덮어쓴다** (LLM이 영상 ID를 지어내는 사고 차단).
- 블록 ②는 `^\s*\d+\.\s*상단:\s*(.+?)\s*/\s*메인:\s*(.+?)\s*(?:—\s*(.+))?$` 로 긁는다.
실패해도 오류가 아니라 빈 리스트 → UI에서 드롭다운만 안 뜬다.
`correct.py``_gemini_key()` / `GeminiQuotaError`를 재사용한다.
#### Step 3 타임코드 기준 보정
구간을 잘라 보낸 클립에 대해 모델이 타임코드를 **원본 기준**으로 줄지 **클립 기준(0부터)**
으로 줄지 보장이 없다(문서에 명시 없음). 어느 쪽이 와도 살아남게 휴리스틱으로 보정한다:
1. 모든 컷이 `[start10, end+10]` 안 → 절대 기준으로 보고 그대로 둔다 (우선)
2. 아니고 모든 컷이 `[0, 클립길이+10]` 안이며 `start > 10` → 클립 기준으로 보고 `start`를 더한다
3. 둘 다 아니면 손대지 않는다 (이후 `parse_paste`·다운로드 단계에서 자연히 드러남)
보정이 일어나면 로그에 남긴다.
### 4.2 `capcut_agent/comments.py`
```python
H_LAB = "https://h-lab.tolag.shop"
def fetch_comments(url, *, timeout=180) -> list[dict]
# POST /api/comment-cards/fetch → data[] 그대로 + idx 부여 + times 계산
TS_RE = r"(?<!\d)(\d{1,2}):([0-5]\d)(?::([0-5]\d))?(?!\d)" # h-lab comment-cards.js 와 동일
def match_window(comments, start, end) -> list[dict] # 구간 언급 댓글, 좋아요 내림차순
def top_liked(comments, exclude_idx, n=20) -> list[dict]
```
- 2조각이면 `mm:ss`, 3조각이면 `h:mm:ss` — h-lab JS 규칙을 그대로 옮긴다.
- 한 댓글에 여러 시각이 있으면 **하나라도 구간 안에 들면 매칭**.
- 댓글 원본에 id가 없으므로 h-lab과 같이 **배열 인덱스**를 식별자로 쓴다.
### 4.3 엔드포인트
| 메서드 | 경로 | 내용 |
|---|---|---|
| POST | `/auto/analyze` | Form `url``{analysis_id}`. 실제 작업은 아래 스트림에서 |
| GET | `/auto/stream/{analysis_id}` | SSE. 기존 `/stream`과 동일한 이벤트(`manifest`/`step`/`log`/`result`/`error`) |
| GET | `/auto/avatar?url=` | 프로필 이미지 프록시. `ggpht.com`·`googleusercontent.com`만 허용 |
| GET·POST | `/prompts` | 프롬프트 두 개 읽기/저장. POST에 `reset=1`이면 기본값 복원 |
| POST | `/auto/build` | multipart. 하이라이트 1개 + 카드 PNG들 → `{job_id}` (기존 `/stream`으로 진행 표시) |
`/auto/stream`의 최종 `result` 이벤트 payload:
```json
{"type":"result",
"highlights":[{"id":1,"start":134.0,"end":242.0,"reason":"…",
"paste":{…parse_paste 결과…},
"titles":[{"top":"…","main":"…","kind":"어그로형"}, ],
"total":52.3,"need":17,
"matched":[12,45,3,], // 댓글 인덱스, 좋아요순
"candidates":[7,19,]}], // 좋아요 상위 20 (matched 제외)
"comments":[{"idx":0,"authorName":"…","text":"…","likeCount":275098,
"replyCount":1000,"publishedAt":"…","profileImageUrl":"…","times":[134.0]}],
"warnings":["h-lab 연결 실패 — 댓글 없이 진행합니다"]}
```
`/auto/build` 폼 필드:
| 필드 | 값 |
|---|---|
| `data` | 하이라이트의 `paste` JSON 문자열 (붙여넣기 탭과 **동일 스키마**) |
| `cards` | PNG 파일 여러 개, 화면에 보인 순서 그대로 |
| `video_scale` `flip` `scene` `bg_white` `remove_silence` `asr_bottom` | 공통 옵션. `/paste`와 동일 |
서버는 `data``parse_paste()`로 검증하고, 카드들을 `.comments/<job_id>/001.png…`
순서대로 저장한 뒤 그 폴더를 `comments_dir`로 하는 job을 만든다. 그 뒤는 **기존
`process_paste()` 경로를 그대로 탄다.**
파이프라인에 허용하는 수정은 딱 하나 — `process_paste(name_suffix="")` 파라미터 추가.
현재 드래프트 이름은 영상 제목으로 덮어써지고(`pipeline.py:340`) pycapcut은
`allow_replace=True`라 **같은 이름이면 이전 드래프트를 교체**한다. 같은 영상에서 5개를
만들면 컷 개수가 같은 하이라이트끼리 서로 덮어쓰므로, `/auto/build``tag`
(예: `하이라이트1`)를 넘겨 이름 뒤에 붙인다. 기본값 `""` → 기존 탭 동작 불변.
또한 `app.py``/static` StaticFiles 마운트를 추가한다 — 현재는 index.html 한 파일만
직접 읽어 주고 있어 `auto.js`·`modern-screenshot.js`를 서빙할 방법이 없다.
### 4.4 프론트엔드
-`🤖 자동` 추가. 기존 `setMode()`에 분기 하나 추가.
- 자동 탭에서는 공통 옵션 중 **댓글 카드 폴더 입력을 숨긴다** (자동 생성 폴더를 쓰므로).
나머지(확대·반전·장면분할·배경흰색·무음제거·하단자막자동)는 그대로 쓴다.
- 카드 렌더는 h-lab `comment-cards.html`의 인라인 CSS 중 카드 부분(`.comment-card`,
`.cc-head`, `.cc-avatar`, `.cc-meta`, `.cc-author`, `.cc-time`, `.cc-text`, `.cc-stats`,
`.mosaic`, `.rounded`, `.bg-black`)만 옮겨온다. 툴바·분석 UI는 안 가져온다.
- 카드 스타일 고정값: **배경 검정 · 모서리 둥금 · 모자이크 ON · 캡처 4배**.
(h-lab 기본값과 동일. 토글은 만들지 않는다 — 필요해지면 그때.)
- 캡처: `modernScreenshot.domToBlob(el, {scale:4, backgroundColor:null})`.
### 4.5 프롬프트 파일
- Step 3 = `숏폼_편집_지침서_v13.7_capcut2연동판.md` (이미 있는 파일을 그대로 읽는다)
- Step 1 = `프롬프트/하이라이트_선정.md` (없으면 기본값으로 생성)
- 모델·fps = `프롬프트/설정.json` (없으면 기본값으로 생성)
```json
{"model": "gemini-3.5-flash",
"model_step1": "",
"fps_step1": 0.2,
"fps_step3": 1.0}
```
(`model_step1`은 비우면 `model`과 동일 — Step 1만 Pro로 올리고 싶을 때 채운다.
실제 파일은 주석 없는 순수 JSON.)
기본 Step 1 프롬프트는 오팔 원문을 옮기되 **오타 하나를 고친다** — 원문 JSON 예시에
`start_time`이 두 번 나오고 `end_time`이 빠져 있다.
자동 탭의 **⚙ 지침 수정** 안에서 프롬프트 2개 + 이 설정을 같이 편집한다.
모델을 바꿔 결과를 비교하는 것이 오팔과의 품질 차이를 좁히는 유일한 수단이므로, 이 값은
숨기지 않고 UI에 노출한다.
## 5. 댓글 매칭 규칙
0. 필요 장수 `need` = §6의 `n_max` = `max(1, floor(컷 총길이 / 3))`. (52.3초 → 17장)
§6과 같은 식을 쓴다 — 화면에 "17장 필요"라고 띄우고 실제로는 18장이 들어가는 일이 없게.
1. 매칭 기준 구간은 **Step 1 윈도우** `[start_time, end_time]` (1분30초~3분).
Step 3의 컷은 순서를 섞어 재배치한 45~60초라, 원본에서 "그 장면"을 가리키는 댓글은
Step 1 윈도우로 잡아야 맞는다.
2. `matched` = 구간 안 타임스탬프를 언급한 댓글, **좋아요 내림차순**. 필요 장수까지 자동 체크.
3. `candidates` = 좋아요 상위 20개 중 `matched`에 없는 것. 부족분을 여기서 사람이 클릭.
4. 하나도 안 골라도 된다 → 댓글 없이 드래프트 생성.
5. 카드 순서 = 화면에 보인 순서(⭐ 먼저, 그다음 고른 순서).
## 6. 카드 배치 규칙 (변경)
`pipeline.py:38 _load_comment_cards`
```
n_max = max(1, floor(전체길이 / 3)) # 3초 밑으로는 안 내려감
n = min(카드 수, n_max) # 초과분은 버림 (지금과 동일)
길이 = 전체길이 / n # 항상 3초 이상, 끝까지 빈 곳 없이 채움
i번째 카드 = [i·길이, (i+1)·길이]
```
| 전체 길이 | 카드 수 | 결과 |
|---|---|---|
| 33초 | 11장 | 3.0초씩 (지금과 동일) |
| 33초 | 6장 | **5.5초씩** — 지금은 18초 뒤가 비었다 |
| 33초 | 20장 | 앞 11장만 3.0초씩 (지금과 동일) |
**기존 3개 탭에도 적용된다.** 카드가 모자랄 때 영상 뒷부분이 비는 문제가 같이 없어진다.
이 설계에서 기존 동작이 바뀌는 유일한 지점이다.
## 7. h-lab 확인 결과 (수정 불필요 근거)
원격 API 실측 (2026-07-31):
```
POST https://h-lab.tolag.shop/api/comment-cards/fetch → 200
{"success":true,"data":[{"authorName":"@YouTube","profileImageUrl":"https://yt3.ggpht.com/…",
"text":"…","likeCount":275098,"replyCount":1000,"publishedAt":"2025-04-22T19:05:08Z"}]}
```
- 인증 없이 열려 있고, 카드에 필요한 필드가 전부 온다
- 프로필 이미지는 **capcut2가 자체 프록시**한다 → h-lab CORS 설정에 의존하지 않고,
같은 출처라 캔버스 오염(taint) 없이 캡처된다
- 카드 CSS는 `comment-cards.html`에서 복사해 온다
**h-lab에 추가할 API도, 고칠 코드도 없다.**
## 8. 실패 처리
각각 독립적으로 죽고, 죽어도 나머지는 살린다.
| 실패 | 처리 |
|---|---|
| Gemini 키 없음 | 자동 탭에 안내 + 붙여넣기 탭 안내. 분석 시작 자체를 막는다 |
| Gemini 429 (무료 한도) | Step 3는 동시 5개 → 429 나온 구간만 순차 재시도(최대 2회). 그래도 실패하면 그 하이라이트만 제외 |
| 무료 티어 유튜브 하루 8시간 초과 | Gemini가 거절 → 메시지 그대로 노출. 원본 1시간짜리면 하루 약 7~8회가 상한 |
| Step 1 컨텍스트 초과 (아주 긴 원본) | `fps_step1`을 더 낮추라는 안내를 띄운다. 0.2 fps 기준 4시간까지는 들어간다 |
| Gemini 응답에 JSON 블록 없음 | 그 하이라이트만 제외 + 원문을 로그에 남김 |
| `parse_paste` 검증 실패 | 그 하이라이트만 제외 + 사유 표시 |
| Step 1이 5개 못 채움 | 나온 것만 진행. 개수를 강제하지 않는다 |
| h-lab 연결 실패·타임아웃 | **댓글 없이 진행.** 하이라이트는 그대로 만든다. `warnings`에 표시 |
| 구간 매칭 댓글 0개 | 후보만 보여준다. 안 고르면 댓글 없이 생성 |
| 카드 캡처 실패 | 그 카드만 빼고 계속 |
| 빌드 5개 중 3번째 실패 | 멈추지 않고 4·5번 계속. 끝에 "4개 성공, 1개 실패" |
전부 실패해도 **오팔 → 붙여넣기 탭** 경로는 그대로 살아 있다.
## 9. 검증
이 프로젝트에 자동 테스트 스위트는 없다. 단계별로:
1. 구문·임포트: `python -c "import ast; ast.parse(open('capcut_agent/plan.py', encoding='utf-8').read())"`,
`python -c "from server import app"`
2. `comments.py` 타임스탬프 파싱은 순수 함수라 손으로 확인:
`"2:14 개웃김"``[134.0]`, `"1:02:03"``[3723.0]`, `"2025년"``[]`
3. 배치 규칙은 `_load_comment_cards`를 직접 불러 §6 표 3줄을 확인
4. 실제 영상 1개로 자동 탭 한 바퀴 → `draft_content.json` 열어 댓글 세그먼트 시간 확인
5. 최종 확인은 **CapCut에서 열어보기**
⚠️ 코드 수정 후 `.bat` 재시작 필수 (hot-reload 없음).
## 10. 오팔과의 품질 차이
프롬프트도 같고, 영상을 유튜브 URL로 넘기는 방식도 같다. **차이가 난다면 원인은 두 가지뿐이다:**
1. **모델** — 오팔이 어떤 모델을 붙이는지는 오팔 편집기에서 노드를 열면 확인된다.
우리 쪽은 §4.5 설정으로 바꾼다. 기본 `gemini-3.5-flash`, 부족하면 `gemini-3.1-pro-preview`.
Step 1만 Pro로 올리는 것도 가능하다(`model_step1`) — 호출이 1회뿐이라 한도 부담이 작다.
2. **영상 샘플링** — Step 1을 0.2 fps로 낮추는 만큼 오팔보다 덜 본다.
짧은 원본만 다루면 `fps_step1`을 올려 동등하게 맞출 수 있다.
검토 화면이 **빌드 전에** 컷·자막·타이틀을 다 보여주므로, 결과가 못 미치면 그 자리에서
설정을 바꿔 다시 분석하거나 오팔로 돌아가면 된다. 되돌릴 수 없는 지점이 없다.
## 11. 열린 항목
- `modern-screenshot`은 구현 시점에 CDN에서 받아 `server/static/`에 동봉하고 버전을 고정한다.
- 첫 실행에서 실제 소요 시간·토큰을 재고, Step 1의 fps 기본값을 그 결과로 조정한다.