추천 근거를 Whisper 받아쓰기 하나로 통일한다. 지금은 자동·붙여넣기 탭이 LLM이 쓴 JSON 자막을, 구간 탭은 분:초만 본다. 자동 탭은 3단계로 나눈다 — 검토 화면의 ✕ 제외 버튼 때문이다. 받아쓰기를 먼저 돌리면 버릴 하이라이트까지 다운로드·받아쓰기하게 되므로, 제외를 먼저 고르게 한다. 파이프라인 두 개(process_paste, process_bg_template)를 모두 analyze/draft 로 쪼개야 해서 구현을 4단계로 끊는다. 1단계는 겉보기 변화 없이 회귀 검증만. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
238 lines
13 KiB
Markdown
238 lines
13 KiB
Markdown
# 받아쓰기 후 댓글 매칭 — 모든 탭 통일 (설계)
|
||
|
||
작성일: 2026-08-04
|
||
관련 코드: `capcut_agent/pipeline.py`, `capcut_agent/recommend.py`, `server/app.py`, `server/static/auto.js`, `server/static/index.html`
|
||
선행 스펙: `2026-08-04-컷별-댓글-추천-design.md` (자동 탭에 컷별 추천 도입. 추천 엔진 `recommend.py`가 여기서 나왔다)
|
||
|
||
## 0. 목표 한 줄
|
||
|
||
**댓글 매칭을 항상 받아쓰기 다음에 한다 — 모든 탭에서 똑같이.**
|
||
|
||
지금은 탭마다 추천 근거가 다르다. 자동·붙여넣기 탭은 LLM이 쓴 JSON 자막(`bottom`),
|
||
구간 탭은 자막이 아예 없어 분:초만 본다. 근거를 **Whisper 받아쓰기 하나로 통일**하면
|
||
정확도가 오르고, 세 탭이 같은 코드로 돌아 유지보수가 단순해진다.
|
||
|
||
| 탭 | 지금 | 바뀐 뒤 |
|
||
|---|---|---|
|
||
| 🤖 자동 | 분석 → 검토(즉시) → 생성 | 분석 → **1차 검토(제목·제외)** → 다운로드·받아쓰기 → **2차 검토(댓글)** → 생성 |
|
||
| ▶ 유튜브 구간 | 매칭(분:초만) → 편집 시작 | 편집 시작 → 다운로드·받아쓰기 → 검토(댓글) → 생성 |
|
||
| 📋 붙여넣기 | 댓글 UI 없음 | 편집 시작 → 다운로드·받아쓰기 → 검토(댓글) → 생성 |
|
||
| 📁 파일 | 폴더 지정 | **제외** — §12 |
|
||
|
||
## 1. 통일된 배정 순서
|
||
|
||
컷(또는 구간) 하나마다 **카드 장수(quota)**만큼 이 순서로 채운다:
|
||
|
||
| 순위 | 근거 | 배지 | 어디서 |
|
||
|---|---|---|---|
|
||
| 1 | 그 컷의 **원본 시각**을 언급한 댓글, 좋아요순 | ⭐ | `match_ranges` |
|
||
| 2 | 그 컷의 **Whisper 자막**을 Gemini가 읽고 고른 댓글 | 🤖 | `ai_pick_cuts` |
|
||
| 3 | 좋아요 상위로 남은 자리 채움 | ➕ | `top_liked` |
|
||
|
||
한 댓글은 한 컷에만 들어간다(겹치면 앞 컷이 가져간다). 이 순서는 이미
|
||
`recommend.build_cut_picks()`에 있고, 3순위만 새로 붙이면 된다.
|
||
|
||
**시각이 어긋나지 않는 경우엔 1순위가 더 정밀해진다.** 통짜 모드와 구간 탭은
|
||
구간을 그대로 이어붙이므로 타임라인 시각 = 원본 시각이 성립한다. 그때는
|
||
`match_slots()`로 3초 슬롯마다 그 시간대 언급 댓글을 꽂는다(기존 동작 유지).
|
||
|
||
**받아쓰기 결과가 없으면**(전부 무음·ASR 실패) 2순위를 건너뛰고 1·3순위만 돈다.
|
||
|
||
## 2. 공통 흐름
|
||
|
||
모든 탭이 결국 같은 모양이 된다:
|
||
|
||
```
|
||
(편집안 확보) → 다운로드·병합 → 무음 분석 → 받아쓰기 → 댓글 매칭 → [검토] → 드래프트
|
||
└───────── analyze 단계 ─────────┘ └ draft 단계 ┘
|
||
```
|
||
|
||
- **편집안 확보**가 탭마다 다르다: Gemini Step1/3(자동 `full`) · 붙여넣은 JSON(자동 `paste`,
|
||
붙여넣기 탭) · URL+구간(구간 탭, 자동 `whole`/`wpaste`)
|
||
- 그 뒤는 전부 같다.
|
||
|
||
## 3. 파이프라인을 둘로 쪼갠다
|
||
|
||
파이프라인이 두 개 있고 **둘 다** 쪼개야 한다.
|
||
|
||
| 지금 | 쪼갠 뒤 | 쓰는 곳 |
|
||
|---|---|---|
|
||
| `process_paste()` | `paste_analyze()` + `paste_draft(state, …)` | 자동 탭(컷 있는 모드), 붙여넣기 탭 |
|
||
| `process_bg_template()` | `bg_analyze()` + `bg_draft(state, …)` | 구간 탭, 파일 탭 |
|
||
|
||
`*_analyze`는 이벤트를 흘리다가 마지막에 `{"type":"state", "state":{…}}`를 내보낸다.
|
||
호출부가 그걸 받아 보관했다가 `*_draft`에 넘긴다.
|
||
|
||
**상태 dict**: `video_path, meta, keep, video_clips, captions, total, placements, draft_name, channel`
|
||
(+ 붙여넣기 계열은 `eff_caps`, `cuts_orig`; 구간 계열은 `ranges_sec`)
|
||
|
||
📁 파일 탭은 두 개를 연달아 부르는 얇은 래퍼 `process_bg_template()`으로 남긴다 —
|
||
**겉보기 동작과 SSE 이벤트가 그대로여야 한다.** 래퍼는 `state` 이벤트를 걸러내 밖으로
|
||
안 흘린다.
|
||
|
||
상태는 `STATES[aid]` 메모리 dict에 둔다. 서버를 재시작하면 사라지는데 기존 `JOBS`도
|
||
같은 성질이라 새 제약이 아니다.
|
||
|
||
## 4. 두 좌표계를 섞지 않는다
|
||
|
||
⚠ 이 설계에서 가장 틀리기 쉬운 지점이다.
|
||
|
||
| 용도 | 좌표계 | 왜 |
|
||
|---|---|---|
|
||
| **1순위 매칭** | **원본 영상 시각** | 댓글에 적힌 `9:05`와 맞춰야 한다 |
|
||
| **자막 추출·배치·장수** | **압축 타임라인** | 무음이 잘린 뒤 실제 자리다 |
|
||
|
||
컷/구간마다 두 값을 나란히 들고 다닌다. `_remap_placements(raw, keep)`가 원본 누적
|
||
구간을 압축 타임라인으로 옮긴다(선행 작업에서 이미 만들어져 있다).
|
||
|
||
```python
|
||
cuts = [{"start": 원본_s, "end": 원본_e, "bottom": 자막}, …] # 매칭용
|
||
places = _remap_placements(raw_places, keep) # 배치용
|
||
quotas = [max(1, int((p1 - p0) // CARD_SEC)) for p0, p1 in places] # 압축 길이 기준
|
||
```
|
||
|
||
## 5. 컷별 자막을 어떻게 얻나
|
||
|
||
받아쓰기 결과 `captions = [(s, e, text)]`는 **압축 타임라인** 기준이다.
|
||
컷 `i`의 자막 = `places[i]` 범위에 걸친 캡션 텍스트를 이어붙인 것
|
||
(공백 정규화, **500자에서 자름** — 그 이상은 Gemini 토큰만 먹는다).
|
||
|
||
이 한 줄로 **세 탭 모두** 자막을 얻는다. JSON `bottom`은 더 이상 추천 근거로 쓰지 않는다
|
||
(화면 자막으로는 그대로 쓰인다 — `asr_bottom` 옵션이 꺼져 있으면).
|
||
|
||
**`asr_bottom` 옵션의 의미가 바뀐다**: 받아쓰기는 추천을 위해 **항상** 돌고,
|
||
이 옵션은 "그 결과를 화면 자막으로도 쓸지"만 정한다. UI 설명 문구를 그에 맞게 고친다.
|
||
|
||
## 6. 추천 엔진 변경
|
||
|
||
`recommend.py`에 필요한 것만 더한다.
|
||
|
||
| 함수 | 변경 |
|
||
|---|---|
|
||
| `build_highlight_cuts(hl, comments, *, key=None, quotas=None)` | `quotas`가 주어지면 `quotas_for()` 대신 그것을 쓴다(압축 길이 기준 장수를 밖에서 계산해 넘긴다). 안 넘기면 기존 동작 |
|
||
| `is_whole(cuts)` → `is_time_based(cuts)` | **모든 컷의 `bottom`이 비면** 시각 기반. 컷 1개 통짜는 특수 케이스가 된다. 구간 2개 이상도 걸린다 |
|
||
| `_whole_picks` | 컷마다 호출하되 `used` 집합을 공유해 컷 간 중복 배정을 막는다 |
|
||
| `build_cut_picks` | 3순위(➕ 좋아요 채움)를 추가한다. 지금은 ⭐·🤖만 채우고 모자라면 비워 둔다 |
|
||
|
||
## 7. 자동 탭 — 3단계
|
||
|
||
검토 화면에 **`✕` ID 제외 버튼**이 있다. 지금은 제외하면 그 하이라이트를 다운로드조차
|
||
안 한다. 받아쓰기를 먼저 돌리면 **버릴 것까지 받아서 받아쓰기**하게 되므로,
|
||
**제외를 먼저 고르게** 한다.
|
||
|
||
```
|
||
① 분석 Gemini Step1/Step3 (또는 JSON 파싱) → 편집안 5개
|
||
② 1차 검토 하이라이트 카드 5장 — 제목 선택, ✕ 제외, 옵션 확인 ← 새 화면
|
||
③ 준비 남은 것만 다운로드·무음·받아쓰기 (순차) + h-lab 댓글 수집
|
||
④ 2차 검토 컷별 댓글 섹션 (지금 검토 화면에서 댓글 부분만)
|
||
⑤ 생성 드래프트 (다운로드·받아쓰기 안 함 — ③에서 끝냈다)
|
||
```
|
||
|
||
②는 지금 검토 화면에서 **댓글 영역만 뺀 것**이라 새로 만드는 게 아니라 나누는 것이다.
|
||
③은 진행 표시를 ID별 보드에 그대로 쓴다.
|
||
|
||
## 8. 구간 탭 · 붙여넣기 탭 — 2단계
|
||
|
||
제외할 것이 없어 1차 검토가 필요 없다.
|
||
|
||
```
|
||
① 편집 시작 다운로드·무음·받아쓰기 (+ h-lab 댓글 동시 수집)
|
||
② 검토 컷/구간별 댓글 섹션
|
||
③ 생성 드래프트
|
||
```
|
||
|
||
구간 탭의 `💬 구간 댓글 매칭` 버튼은 없어진다(`편집 시작`이 그 일을 한다).
|
||
제목·출처는 검토 화면에서도 고칠 수 있게 둔다 — 1분 넘게 기다린 뒤 오타를 발견하면
|
||
다시 돌리는 게 낭비다.
|
||
|
||
## 9. 엔드포인트
|
||
|
||
| 엔드포인트 | 상태 |
|
||
|---|---|
|
||
| `POST /auto/analyze` → `GET /auto/stream/{aid}` | 유지. **댓글 매칭을 빼고** 편집안까지만 |
|
||
| `POST /auto/prepare` (신규) | 남은 ID들 → 다운로드·받아쓰기·댓글 매칭. SSE로 진행, `result`에 ID별 `cuts[]` |
|
||
| `POST /auto/build` | 유지. `card_cuts` 이미 받음. 이제 상태를 재사용해 드래프트만 |
|
||
| `POST /yt/analyze` → `GET /yt/stream/{aid}` (신규) | 구간 탭 1단계 |
|
||
| `POST /yt/build` (신규) | 구간 탭 3단계 |
|
||
| `POST /paste/analyze` → `GET /paste/stream/{aid}` (신규) | 붙여넣기 탭 1단계 |
|
||
| `POST /paste/build` (신규) | 붙여넣기 탭 3단계 |
|
||
| `POST /yt/comments`, `POST /youtube` | **삭제** (새 흐름이 대체. 다른 호출부 없음을 grep으로 확인할 것) |
|
||
| `POST /upload`, `POST /paste`, `GET /stream/{job_id}` | 유지 |
|
||
|
||
`result` 이벤트 스키마는 선행 스펙과 같다(`cuts[].picks[].why` = `ts`/`ai`/`like`).
|
||
|
||
## 10. 렌더러를 하나로 모은다
|
||
|
||
카드 패널을 그리는 코드가 지금 자동 탭(`onResult`)과 구간 탭(`ytMatch`) 두 갈래다.
|
||
붙여넣기 탭까지 더하면 세 갈래가 되어 한 곳만 고치는 실수가 난다.
|
||
|
||
**`renderCutPanel(panelId, data, opts)` 하나로 모은다** — 컷/구간별 섹션, ⭐🤖➕ 배지,
|
||
검색창, `➕ 채우기` 섹션, 컷별 선택 상한을 전부 담당한다. 세 탭이 `panelId`만 달리해
|
||
부른다(`hl.id` / `"yt"` / `"paste"`). 라벨 접두사(`컷` vs `구간`)는 `opts`로 넘긴다.
|
||
|
||
이번 작업에 필요해서 하는 정리이지 무관한 리팩터링이 아니다.
|
||
|
||
## 11. 실패·폴백
|
||
|
||
| 상황 | 처리 |
|
||
|---|---|
|
||
| 받아쓰기 결과 없음 | `bottom` 전부 빔 → `is_time_based` 경로(시각 슬롯 + 좋아요 채움) |
|
||
| Gemini 실패(429·타임아웃·파싱) | `ai_failed` 경고 한 줄 + ⭐·➕만으로 배정 |
|
||
| h-lab 댓글 수집 실패 | 경고 한 줄 + 댓글 없이 검토 화면 → 카드 없이 생성 가능 |
|
||
| 다운로드 실패 | 기존과 동일 — `error` 이벤트로 중단(영상이 없으면 만들 수 없다) |
|
||
| `aid` 만료(서버 재시작) | build가 404 + "분석을 다시 돌려주세요" |
|
||
|
||
**다운로드 실패를 빼면 어떤 경우에도 드래프트 생성을 막지 않는다.**
|
||
|
||
## 12. 하지 않는 것
|
||
|
||
- **📁 파일 탭** — 로컬 파일이라 댓글을 가져올 유튜브 URL이 없다. URL을 따로 받으면
|
||
자막 기반 추천(🤖)은 가능하지만, 로컬 파일의 시각이 원본 영상 시각과 달라
|
||
**분:초 매칭(⭐)은 원리적으로 불가**하다. 사용자가 제외를 선택했다.
|
||
- **📁 파일 탭 흐름 변경** — 내부만 쪼개고 겉보기는 그대로.
|
||
- **유튜브 자동자막** — Whisper 받아쓰기를 쓰므로 불필요.
|
||
- **다운로드 캐시 추가** — 파이프라인을 쪼개면 재다운로드가 없어진다.
|
||
- **하이라이트 병렬 준비** — yt-dlp·ffmpeg·Whisper가 CPU를 다 쓴다. 순차 유지(기존 방침).
|
||
- **구간별 제목·출처 개별 지정** — 이어붙여 영상 하나를 만드므로 제목도 하나다.
|
||
|
||
## 13. 구현 단계
|
||
|
||
한 번에 다 하기엔 크다. **각 단계가 그 자체로 돌아가는 상태**로 끊는다.
|
||
|
||
| 단계 | 내용 | 끝나면 |
|
||
|---|---|---|
|
||
| **1** | 파이프라인 분할(`*_analyze`/`*_draft`) + `recommend.py` 변경 + 렌더러 통합 | 겉보기 동작 불변. 회귀만 확인 |
|
||
| **2** | ▶ 구간 탭 2단계 적용 | 구간 탭에서 받아쓰기 후 매칭이 돈다 |
|
||
| **3** | 📋 붙여넣기 탭 2단계 적용 | 붙여넣기 탭에 댓글 화면이 생긴다 |
|
||
| **4** | 🤖 자동 탭 3단계 적용 | 세 탭 통일 완료 |
|
||
|
||
단계마다 계획을 따로 쓰고 실행한다. 1단계는 사용자에게 보이는 변화가 없으므로
|
||
**회귀 검증이 전부**다 — 여기서 깨지면 나머지가 전부 위에서 무너진다.
|
||
|
||
## 14. 검증
|
||
|
||
자동 테스트 스위트가 없다(CLAUDE.md).
|
||
|
||
**순수 함수 (인라인 assert)**
|
||
1. `is_time_based()` — 자막 전부 비면 참, 하나라도 차 있으면 거짓, 컷 1개 통짜도 참
|
||
2. 컷 간 중복 배정 없음 (`_whole_picks` 공유 `used`)
|
||
3. 컷 자막 추출 — 압축 좌표 범위에 걸친 캡션만, 500자에서 자름
|
||
4. `quotas` 외부 주입 시 `build_highlight_cuts` 동작(안 넘기면 기존과 동일)
|
||
5. `build_cut_picks` 3순위(➕) 채움
|
||
|
||
**회귀 (1단계에서 반드시)**
|
||
6. 📁 파일 탭: 분할 전후 SSE 이벤트 순서·내용 동일, `state` 이벤트가 밖으로 안 샘
|
||
7. 🤖 자동 탭: 렌더러 통합 후 컷별 섹션·배지·검색·캡처 그대로
|
||
8. 네 탭 모두 드래프트가 예전과 같은 결과로 나오는가
|
||
|
||
**실사용 (단계별)**
|
||
9. 구간 2개 영상 → 구간별 섹션·자동 선택, 카드 시간이 각 구간 압축 범위 안
|
||
10. 표시한 `need` = 실제 배치된 카드 수(버려지는 카드 0)
|
||
11. 붙여넣기 탭 → 컷별 섹션, 댓글 안 골라도 생성됨(하위호환)
|
||
12. 자동 탭 3단계 → 1차에서 2개 제외 시 그 2개는 다운로드·받아쓰기 안 함
|
||
13. Gemini 키를 비워 세 탭 모두 폴백이 도는가
|
||
|
||
## 15. 열린 질문
|
||
|
||
없음. 미결이 생기면 여기에 적고 구현 전에 사용자에게 묻는다.
|