Commit Graph

41 Commits

Author SHA1 Message Date
bfba87288f docs: 2~4단계 구현 계획 — 세 탭 모두 받아쓰기 후 댓글 매칭
세 탭이 결국 같은 흐름이 되므로 서버 조립 로직을 recommend.cuts_from_state() 하나로
모으고 화면은 이미 통합된 renderCutPanel() 을 재사용한다. 탭마다 다른 건
편집안을 어디서 얻느냐뿐이다.

가장 위험한 지점은 좌표계 둘(원본 시각=매칭, 압축 타임라인=자막·배치·장수)을
섞는 것이라 계획 전반에 명시했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 10:25:19 +09:00
7c74fec079 fix: 컷별 댓글 배정을 컷 우선이 아닌 순위(라운드) 우선으로 변경
컷 하나를 4순위까지 다 채운 뒤 다음 컷으로 넘어가면, 앞 컷의 약한 근거(4순위
좋아요)가 뒤 컷의 강한 근거(3순위 단어 겹침)보다 먼저 댓글을 가로챈다. 라운드를
바깥 루프로 두어(전 컷의 ts → 전 컷의 ai → 전 컷의 word → 전 컷의 like) 순위가
컷 순서를 이기게 했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 18:47:55 +09:00
011c042778 자동 탭: 🔤 단어겹침 배지 표시 + AI 추천 실패 경고 문구 명확화
WHY 배지 맵에 word:"🔤" 를 더해 3순위(단어 겹침)로 배정된 카드가 화면에서
구분되게 했다(다른 곳엔 why 값을 쓰는 곳이 없어 auto.js 한 곳만 고치면 된다).
"AI 추천 실패 — 타임스탬프만으로 배정했습니다" 문구는 로그 영역에 묻혀
사용자가 못 보던 경고였다. 무엇이 실패했고(Gemini 응답 없음) 그래서 어떻게
됐는지(분:초·자막단어·좋아요로 대체)가 한 문장에서 드러나도록 고쳤다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 18:43:39 +09:00
b1c08908ae 컷별 댓글 추천에 단어 겹침(3순위)·좋아요(4순위) 폴백 추가
Gemini 가 실패하거나(503 등) 문맥은 맞지만 겹치는 단어가 없어 못 잡는
경우("와인 뱉는 장면" ↔ "싱크대로 달려간 이유")가 각각 달라, 대체가 아니라
시각 » 🤖AI 뒤에 한 단계 더 얹었다. 자막에서 조사를 뗀 어간까지 키워드로
뽑아 댓글 본문에 부분 문자열로 걸리면 매칭한다("넉살이"→"넉살"이 "넉살님"에
걸리는 게 핵심 케이스). 네트워크 없이 항상 동작해 Gemini 가 죽어도 0건은
안 나온다. 그래도 자리가 남으면 분:초 없는 댓글 중 좋아요 상위로 채운다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 18:43:19 +09:00
9ea70d01a7 자동 탭 컷별 추천 모델 교체(gemini-3.5-flash) + 429/503 재시도
gemini-2.5-flash 가 HTTP 503("high demand")을 자주 반환해(실측 3연속 503)
ai_pick_cuts 가 재시도 없이 즉시 포기, 하이라이트의 컷 전체가 🤖 0개가 되는
증상의 근본 원인이었다. plan.py 가 이미 쓰는 gemini-3.5-flash 로 모델을
통일하고, 429/503 한정으로 3초·8초 간격 재시도(최대 3회 시도)를 추가했다.
그 외 오류와 재시도 소진 시에는 기존과 동일하게 None 을 반환한다(예외 미노출).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 18:42:57 +09:00
058f03265c 리뷰 반영: selonly 캡처 함정 차단, 더보기/검색 재렌더 링 오류, 검색 중 접힌 컷 결과 숨김 수정
Critical: wrapOf가 ci를 몰라 '선택한 것만 보기' 중 배정 안 된(=display:none) 쌍둥이를
캡처하는 조용한 실패가 있었다. wrapOf(hlId,idx,ci)로 정확한 복사본을 찾게 하고,
buildAll에서 selonly를 이중으로 꺼서 방어한다. 칩 클릭 스크롤도 같은 방식으로 고쳤다.

Important: cardSection.render()가 컷 판정 없이 링을 붙여 '더보기'·검색 재렌더 때 증상이
되살아났다 — refreshSel과 같은 판정식을 render()에 직접 넣었다(패널이 아직 document에
안 붙어 있는 초기 구성 중에도 정확히 동작해야 하므로 refreshSel 재호출 대신 선택).

Important: 검색이 섹션 단위로만 display를 토글해 닫힌 컷 아코디언 안의 결과가 안 보였다.
검색 중엔 그 패널의 컷을 전부 펼치고, 지우면 검색 시작 전 열림 상태로 되돌린다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 18:13:00 +09:00
d1ebfaff74 자동 탭: 컷별 쌍둥이 카드 혼동 해소 + 컷 섹션 아코디언
같은 댓글이 컷마다 반복 렌더되는데 카드 DOM에 컷 번호가 없어 refreshSel()이
모든 복사본에 선택 링을 붙였다(1컷에서 고른 게 2컷에도 체크된 것처럼 보임).
쌍둥이를 눌러도 다른 컷으로 옮기려는 의도인데 무조건 해제됐다.

- cardEl에 data-ci를 남기고, refreshSel은 실제 배정된 컷의 복사본에만 링을 부여
- 다른 컷에 배정된 쌍둥이는 기존 .ccused 배지 구조를 재사용해 "컷 N에서 사용중"으로 표시
- 이미 선택된 쌍둥이를 다른 컷에서 누르면 해제 대신 그 컷으로 이동(sortSel로 순서 유지)
- 컷마다 추천+채우기 섹션을 아코디언으로 묶어 한 번에 한 컷만 펼침(카드 폭증 완화),
  헤더에 컷 채움 상태를 실시간 표시
- 빌드 캡처 직전 expandAllCuts()로 모든 컷 섹션을 강제로 펼침
  (검색 필터 때와 같은 함정 — 접힌 섹션의 카드는 domToBlob이 빈 이미지로 찍는다)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 18:02:55 +09:00
0967e88e7b 문서: 파이프라인 analyze/draft 분할과 컷별 댓글 추천 근거를 ARCHITECTURE.md에 반영
댓글 매칭을 받아쓰기 뒤로 옮기려고 미리 파이프라인을 analyze/draft 두 조각으로
쪼갠 구조라, 왜 그렇게 했는지·state 이벤트는 내부 전용이라는 것·manifest는
래퍼가 낸다는 함정을 남기지 않으면 다음 사람이 되돌리기 쉽다. captions_for_places와
recommend.py의 is_time_based/quotas 변경, auto.js 렌더러 통합도 같은 이유로 기록.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:46:06 +09:00
e79f2dfaa3 process_paste를 paste_analyze/paste_draft 두 조각으로 분할
댓글 매칭을 받아쓰기(asr_bottom) 다음에 넣으려면 파이프라인이
"받아쓰기까지" 상태에서 한 번 멈출 수 있어야 한다. 앞서 process_bg_template에
적용한 것과 같은 패턴(analyze/draft 분리 + state 이벤트 + 얇은 래퍼)을
process_paste에도 적용했다. 로직은 옮기기만 했고 바꾸지 않았다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:36:50 +09:00
475b7b0f71 process_bg_template을 bg_analyze/bg_draft로 분할
댓글 매칭을 받아쓰기 다음 단계에 끼워 넣으려면 파이프라인이 "받아쓰기까지"에서
한 번 멈출 수 있어야 한다. 다운로드~asr(bg_analyze)과 장면분할~draft(bg_draft)
두 조각으로 나누고, 기존 동작을 완전히 보존하는 얇은 래퍼(process_bg_template)로
다시 감쌌다. 이벤트 스트림을 분할 전/후로 떠서 문자 단위로 비교해 11개 이벤트가
완전히 동일함을 확인했다(state 이벤트는 밖으로 새지 않음).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:28:54 +09:00
22a9758e74 auto.js: 카드 패널 렌더러를 renderCutPanel()로 통합
onResult(자동 탭)과 ytMatch(구간 탭)에 각각 있던 검색창+컷별/ 섹션+후보
렌더 블록이 두 갈래로 갈라져 있어, 곧 붙여넣기 탭까지 더해지면 한 곳만
고치는 실수가 나기 쉬웠다. 두 블록을 renderCutPanel(box,panelId,data,opts)
하나로 합쳐 배지·검색·선택 상한 로직을 한 곳에 모았다.

화면 문구는 완전히 동일하게 보존했다 — 특히  폴백 라벨은 onResult가
"이 구간을", ytMatch가 "구간을"로 이미 서로 달랐기에, unit 값에 따라
원문 그대로 재현하도록 분기했다(무작정 unit을 문자열에 꽂지 않음).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:20:21 +09:00
aab2a3199b 컷별 자막 추출 헬퍼 함수 추가 (captions_for_places)
받아쓰기(ASR) 결과에서 컷 구간별로 자막을 이어붙이는 순수 함수.
- 시간축 겹침 판정: 닿기만(경계) 제외, 조금이라도 겹치면 포함
- 공백 정규화: 여러 칸 → 공백 하나
- 토큰 제한: cap 문자에서 자르기 (Gemini 효율)
- 테스트 모두 통과 (경계 규칙·공백·길이·엣지 케이스)

다음 단계(2단)에서 세 탭이 모두 호출할 예정.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:12:01 +09:00
a1f98b0b53 무음 제거 후 실제 길이 기준 카드 장수를 외부에서 주입 가능하게
build_highlight_cuts()에 quotas 파라미터 추가. 무음 제거 후 줄어든 타임라인에 맞춰
카드 장수를 계산한 후 넘길 수 있다. quotas를 넘기지 않으면 기존 동작(원본 길이 기준)
그대로여야 한다 — 기존 호출부에는 변화 없다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:06:35 +09:00
2525fb5231 시각 기반 판별을 컷 1개에서 모든 컷으로 일반화
- is_whole() → is_time_based(): 컷 개수와 무관하게 모든 자막이 비었으면 참
  * 통짜 모드(1개 컷) + 유튜브 구간 탭(2개 이상 자막 없는 구간)을 둘 다 지원
  * 자막 없으면 내용 추천 불가 → 시각 기반으로 배정
- _whole_picks() → _time_based_picks(): 여러 컷 반복 처리
  * used 집합을 컷 사이에 공유해 댓글 중복 금지 (앞 컷 우선)
  * 각 컷의 quota에 맞춰 슬롯 배정, 빈 슬롯은 좋아요 상위로 채움
- build_highlight_cuts() 호출부 업데이트
  * 기존 통짜 모드는 그대로 동작 (1개 컷 + 빈 자막 = 참)
  * 새 규칙이 유튜브 구간 탭으로 확장

검증:
- 자막 판별 조건 (empty/whitespace 포함 처리)
- 컷 간 중복 금지 확인
- 자동 탭 회귀 테스트 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:59:33 +09:00
6df1930d06 docs: 1단계 구현 계획 — 파이프라인 분할 + 추천 엔진 준비 + 렌더러 통합
겉보기 동작이 하나도 안 바뀌는 구조 변경이라, 분할 전 이벤트 스트림을 파일로 떠 두고
분할 후와 문자 그대로 비교하는 방식으로 회귀를 검증한다(Task 4·5).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:55:31 +09:00
ef78702ca9 docs: 스펙 전면 개정 — 모든 탭이 받아쓰기 후 댓글 매칭
추천 근거를 Whisper 받아쓰기 하나로 통일한다. 지금은 자동·붙여넣기 탭이
LLM이 쓴 JSON 자막을, 구간 탭은 분:초만 본다.

자동 탭은 3단계로 나눈다 — 검토 화면의 ✕ 제외 버튼 때문이다. 받아쓰기를 먼저
돌리면 버릴 하이라이트까지 다운로드·받아쓰기하게 되므로, 제외를 먼저 고르게 한다.

파이프라인 두 개(process_paste, process_bg_template)를 모두 analyze/draft 로
쪼개야 해서 구현을 4단계로 끊는다. 1단계는 겉보기 변화 없이 회귀 검증만.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:35:13 +09:00
2d37d42893 docs: 스펙 범위를 구간 탭 + 붙여넣기 탭으로 확장
댓글 매칭이 일어나는 모든 곳에 같은 추천·배치를 적용한다. 붙여넣기 탭은
자막이 JSON 안에 있어 파이프라인·추천 엔진을 안 건드리고 화면만 붙이면 되므로
같은 스펙에 담는다. 카드 패널 렌더러가 세 갈래로 갈라지는 걸 막기 위해
renderCutPanel 하나로 모으는 것도 포함.

파일 탭은 제외 — 로컬 파일이라 원본 영상 시각을 몰라 분:초 매칭이 불가하다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:19:22 +09:00
4abf41ebcb docs: 구간 탭 2단계 분할 + 구간별 댓글 추천 스펙
받아쓰기가 빌드 중간에 있어 카드 고를 때 자막이 없다. 파이프라인을 쪼개
받아쓰기 다음에 고르게 하면 자막 품질·장수 정확도·중복 작업이 한 번에 풀린다.
다운로드 캐시가 없어 '매칭 때 받아쓰기, 빌드 때 캐시' 방식은 시간이 두 배가 된다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:08:29 +09:00
3d3f6f6b14 merge: 컷별 댓글 추천 + 댓글 검색
자동 탭에 컷별 댓글 추천·컷 위치 배치를 넣고, 댓글 선택 화면에 검색을 추가한다.
태스크별 리뷰 10회 + 브랜치 전체 리뷰(+수정 웨이브)를 거쳤다.
실사용 검증(실제 영상 → CapCut에서 열어 확인)은 아직 안 했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:05:52 +09:00
20a7c0c0a2 feat: 댓글 선택 화면에 검색(필터) 추가
카드가 수백~수천 장이라 원하는 댓글을 눈으로 찾기 어려웠다. 패널마다 검색창을
두고 모든 카드 섹션이 자기 전체 목록에서 필터링한다 — '더보기'로 안 펼친 카드도
검색되면 나온다.

캡처 누락 방지: 빌드/캡처는 wrapOf() 로 DOM에서 카드를 찾고 없으면 조용히
건너뛴다. 검색으로 선택 카드가 DOM에서 빠질 수 있으므로 buildAll 과
ytCC.capture 시작 시 clearSearch() 로 강제 해제한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:05:41 +09:00
93fa0a3a21 docs: 스펙 §4.4 를 수정 후 실제 배치 방식에 맞게 갱신
무음 제거 시 카드 '시간'을 _remap_caps 로 옮긴다고 적혀 있었으나, 그러면
cards_fixed(3초 고정)가 압축되며 깨진다. 실제로는 _remap_placements 로
컷 '구간'을 먼저 옮기고 그 안에서 나눈다. 장수 캡과 버림 로그도 명시.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 15:06:30 +09:00
c39b72518f 컷별 카드 장수 공식을 floor로 통일 — round와 어긋나 카드가 조용히 버려지던 문제 수정
recommend.quotas_for()는 round, pipeline._cards_by_cut()의 컷당 장수 상한은 floor를
써서 둘이 어긋났다. 5초 컷처럼 round가 floor보다 큰 쪽으로 갈리는 컷은 UI가 카드
2장을 고르게 하고도 빌드 단계에서 상한(1장)에 걸려 뒤 1장이 로그 없이 버려졌다.
사용자 결정에 따라 floor로 통일(카드 한 장이 항상 3초 이상 — 기존
_load_comment_cards 규칙과 동일)하고, 무음 제거로 컷이 압축돼 여전히 카드가
버려지는 경우(이건 불가피)를 컷별 배치 로그에 표시해 더 이상 조용히 사라지지
않게 했다. 관련 설계/계획 문서의 공식·테스트 기대값도 floor 기준으로 맞췄다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 15:05:14 +09:00
754692a343 문서 3개를 실제 동작에 맞게 고침
계획서: "ci 가 undefined 면 마지막 컷 뒤에 붙는다, 배치상 문제 없다"는 근거가 틀렸다.
_cards_by_cut 은 컷 뒤가 아니라 컷 안에서 균등 분할한다 — 마지막 컷에 몰리면 번쩍인다.
왜  를 컷 안으로 흡수했는지로 다시 썼고 Step 3/4 코드도 실제 구현으로 맞췄다.

ARCHITECTURE.md: "카드도 자막과 같은 _remap_caps() 로 재매핑" → 실제로는
_remap_placements() 로 컷 구간을 옮기고 그 안에서 나눈다(무음 제거 뒤에 계산).
3초 하한 규칙도 명시.

SETUP.md: "컷 섹션이 안 보이고  하나만 뜬다 | Gemini 한도 초과" 는 나오지 않는 증상이다
— 한도를 넘겨도 컷 섹션은 그대로 뜨고 🤖 배지만 사라진다. 또 경고는 "검토 화면 상단"이
아니라 로그 영역(alog)으로 나간다. 두 행을 실제 증상 4행으로 다시 썼다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:16:45 +09:00
c6092a93ac 섹션을 컷 안으로 흡수 — 고른 카드가 마지막 컷에 처박히지 않게
독립 " 좋아요 상위 후보" 섹션은 cardSection 에 ci 를 안 넘겨서
toggle(hlId,idx,undefined) → selCut 에 기록 안 됨 → 빌드 때 마지막 컷으로 폴백했다.
계획서는 "맨 뒤로 가니 마지막 컷 뒤에 붙는다, 배치상 문제 없다"고 적었는데 틀렸다.
_cards_by_cut 은 컷 뒤에 붙이지 않고 **컷 안에서 균등 분할**한다 —
4초짜리 마지막 컷에 5장이 들어가면 0.8초씩 번쩍인다.

AI 프롬프트가 "억지로 채우지 마라, 없으면 빈 배열로 둬라"라고 지시하므로
quota 미달이 기본 동작이고, 스펙 §7 은 그 미달을 "사람이 에서 채운다"로 풀었다.
즉 이건 예외가 아니라 주 워크플로다.

- hl.cuts 가 있으면 컷마다 두 섹션(둘 다 ci=cu.i):
  ① 컷 N · X초 · 카드 Q장 — 자막   ② ↳ 좋아요 상위에서 채우기
  ②는 컷마다 반복되므로 초기 렌더를 6장(CUT_FILL_PAGE)으로 줄이고 나머지는 더보기.
- 마지막 컷 폴백 제거. ci 가 없으면 엉뚱한 컷에 넣지 말고 그 카드를 건너뛴다
  (fd.append("cards") 와 sentCuts.push 는 반드시 쌍으로 — 하나만 돌면 전부 어긋난다).
- hl.cuts 가 없는 폴백 경로( + 독립 )는 그대로 둔다.

같이: 컷 섹션의 '나머지'를 그 컷 시간대 언급 댓글로 좁혔다.
hl.matched 전체를 컷마다 복제하면 컷 12개 × 30장 = 360장, 하이라이트 5개면 1800장으로
부풀고(기존 300장) 같은 댓글이 여러 섹션에 중복돼 한쪽을 고르면 쌍둥이까지 하이라이트됐다.
데이터(byIdx[i].times, paste.cuts[i].start/end)는 이미 클라이언트에 다 있다.
호출되지 않는 죽은 함수 cutOf() 도 제거.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:15:06 +09:00
499223a125 카드 배치를 무음 제거 뒤로 옮겨 3초 약속을 지킨다
카드 시간을 무음 제거 **전** 타임라인에서 만들고 _remap_caps 로 압축하고 있었다.
자막은 그게 맞다(발화 시각을 따라가야 하니까). 하지만 카드는 다르다:

- cards_fixed=True 는 "카드는 정확히 3초"라는 절대 길이 약속이라 압축하면 깨진다.
  컷 한가운데가 무음이면 3.0초짜리가 2.0초로 눌린다.
- fixed=False 에서도 3초 하한이 사라져 있었다. 기존 _load_comment_cards 는
  n = max(1, int(dur // 3)) 으로 카드당 3초를 보장하는데 _cards_by_cut 엔 그게 없었고,
  quota 는 **원본** 컷 길이로 계산되므로 무음이 절반인 24초 컷은 8장 → 압축 12초 →
  장당 1.5초가 됐다.

고친 방식:
- _remap_placements() 추가 — 카드는 '시간'이 아니라 '컷 구간 자체'를 옮긴다.
- _cards_by_cut 호출을 무음 제거 블록 뒤로 옮기고 압축된 구간·timeline_dur 로 계산.
  무음 제거가 꺼져 있으면 card_places = placements 그대로다.
- _cards_by_cut 에 컷당 장수 상한 min(len, max(1, floor(컷길이/min_sec))) 추가.
  초과분은 버린다 — _load_comment_cards 와 같은 규칙.
- 이제 필요 없어진 cut_cards 의 _remap_caps 호출 제거.

기존 탭(process_bg_template)과 _load_comment_cards 는 손대지 않았다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:12:31 +09:00
4e479b50ce 추천 단계에 진행 표시 추가 (최대 450초 무반응 제거)
/auto/stream 은 하이라이트 5개를 순차로 돌며 각각 Gemini 를 부른다(timeout=90).
manifest 에 recommend 스텝이 없고 루프 안에 로그도 없어서, 마지막으로 보이는 게
"댓글 수집 done" 이고 그 뒤 최악 450초 동안 아무 이벤트도 안 나갔다.
사용자가 멈춘 줄 알고 새로고침하면 분석이 통째로 날아간다.

순차 호출 자체는 유지한다 — 동시에 5발을 쏘면 429 가 난다(Step 3 도 같은 이유로
시차 재시도를 쓴다). 대신 보이게만 만든다:
- 세 갈래 manifest(paste / wpaste / else) 전부에 recommend 스텝 추가
- 루프 앞뒤로 step start/done(detail = 처리한 하이라이트 수)
- 하이라이트마다 "ID n 컷별 댓글 추천 중… (i/N, 컷 M개)" 로그 한 줄

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:09:41 +09:00
1e43530933 Gemini 추천 실패를 화면에 알린다 (조용히 삼키지 않음)
ai_pick_cuts 가 429·타임아웃·JSON 파싱 실패를 전부 {} 로 삼켜서,
build_highlight_cuts 는 여전히 타임스탬프만으로 채운 cuts 를 돌려주고
app.py 의 `if cuts:` 가 참이 되어 except 가 절대 타지 않았다.
스펙 §7이 요구한 "⚠ 추천 실패 → 기존 방식" 경고가 화면에 안 떴다.

- ai_pick_cuts: 실패는 None, 성공은 dict(빈 dict 포함).
  키가 없는 경우는 실패가 아니다 — 키 없이 쓰는 것도 정상 사용이라 {} 를 준다.
  실패로 치는 건 HTTP·타임아웃·응답 파싱·프롬프트 치환 오류뿐.
- 응답 파싱을 _rows_to_picks() 로 떼어내 '깨진 응답'(None)과
  '어울리는 게 없어 빈 배열'({})을 구분한다. _parse_ai() 계약은 그대로 둔다.
- build_highlight_cuts → (cuts, need, ai_failed) 3-튜플.
  통짜 모드는 Gemini 를 안 부르므로 ai_failed=False.
- build_cut_picks 는 ai_picks=None 이어도 안전(`ai_picks or {}`).
- app.py: ai_failed 면 warnings 에 "AI 추천 실패 — 타임스탬프만으로 배정" 한 줄.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:09:19 +09:00
6f90efd2bc docs: 컷별 댓글 추천 기능을 ARCHITECTURE/SETUP에 반영
recommend.py 모듈, 카드 배치가 자동 탭(card_cuts→_cards_by_cut)과
나머지 탭(_load_comment_cards 전체 균등)으로 갈린 이유, 그리고 서버가
카드 시간을 미리 확정하지 않는 이유(무음 제거 시 자막만 재매핑되고
카드가 혼자 어긋나는 것을 방지)를 문서에 남겨 나중에 이 설계를
실수로 되돌리는 것을 막는다. SETUP.md 문제 해결표에도 추천 실패
폴백 증상 두 건을 추가.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 12:38:36 +09:00
5b58e94ad1 검토 화면을 컷별 카드 묶음으로 렌더
서버가 하이라이트마다 컷 단위 추천(hl.cuts)을 내려주기 시작해서(6~8번
커밋), 화면도 컷 소속을 알아야 카드를 그 컷 구간 안에 배치할 수 있다.
카드가 어느 컷에 속하는지(selCut)를 추적해 컷별 상한(quota)을 걸고,
sel[hlId]를 항상 컷 순서로 정렬해 업로드 파일명(001.png…)과
card_cuts 배열 인덱스가 어긋나지 않게 했다. hl.cuts가 없으면(추천
실패) 기존  통짜 화면으로 그대로 폴백한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 12:28:11 +09:00
c3556edb9d /auto/build가 card_cuts 폼 필드를 받아 파이프라인에 넘김
- auto_build 시그니처에 card_cuts 폼 필드 추가
- JSON 배열 문자열 파싱 (값 깨져도 빌드 계속)
- job dict에 저장 후 process_paste 호출에 전달
- bool 필터링 강화: JSON true/false가 정수로 둔갑하지 않도록

이유: JSON의 true/false가 Python bool로 파싱되는데, bool이 int의 서브클래스라
isinstance(v, int)를 통과해 컷 인덱스 1/0으로 대신 쓰이는 문제 해결.
capcut_agent/recommend.py 패턴과 일관성 있음.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 12:23:18 +09:00
0a0b43efc2 댓글 카드를 컷 구간 안에 배치하는 경로 추가
_load_comment_cards를 _card_paths/_load_comment_cards/_cards_by_cut로 분리했다.
자동 탭은 카드가 어느 컷 소속인지(card_cuts)만 넘기면 그 컷 구간 안에서 균등
배치되어, 한 컷의 카드가 모자라도 다음 컷 카드가 앞으로 밀리지 않는다(기존
전체 균등 배치의 약점). 카드 시간은 무음 제거 전 placements 직후에 계산하고,
자막과 동일하게 _remap_caps()로 재매핑한다 — 카드 튜플(start,end,path)이
자막 튜플(start,end,text)과 모양이 같아 그대로 재사용 가능하다. card_cuts가
없으면 기존 _load_comment_cards 폴백으로 떨어져 파일/유튜브 구간 탭은 영향
없다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 12:18:33 +09:00
90ff2b174f /auto/analyze 결과에 컷별 추천(cuts[]) 추가
댓글 매칭 후 recommend.build_highlight_cuts()를 호출해 하이라이트별
cuts/need를 SSE result에 실어 보낸다. Gemini 호출이 블로킹이라
asyncio.to_thread로 스레드에서 돌리고, 실패해도 기존 matched/candidates로
화면이 폴백하도록 warnings에만 기록하고 드래프트 생성은 막지 않는다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 12:12:35 +09:00
005beba4c3 하이라이트 조립 — build_highlight_cuts() 모드 분기 구현
통짜 모드(컷 1개 + 자막 없음)는 시각 슬롯으로 배정, 컷 있는 모드는 Gemini 추천.
is_whole() 판별, _whole_picks() 슬롯 채우기, build_highlight_cuts() 조립.
import에 match_slots, top_liked 추가.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 12:07:48 +09:00
88768020bf docs: 계획서 Task 4 예시 except 튜플의 예외 누출 수정
리뷰에서 잡힌 구멍이 계획서 참고 코드에도 그대로 있었다. str.format 의
ValueError 와 .decode 의 UnicodeDecodeError(= ValueError 서브클래스)가
빠져 있어, 그대로 옮기면 "어떤 실패에도 예외 없음" 계약이 깨진다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 12:05:05 +09:00
ee30e87bb4 예외 처리 개선: ValueError와 UnicodeDecodeError 추가
- str.format() 예외 처리에 ValueError 추가 (템플릿 중괄호 불균형)
- .decode(utf-8) 예외 처리에 ValueError 추가 (UnicodeDecodeError는 ValueError 서브클래스)
- URLError/TimeoutError는 이미 OSError 서브클래스이므로 정리 (주석 추가)
- ai_pick_cuts는 모든 실패에 예외 없이 {} 반환하는 계약 이행

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 12:02:36 +09:00
7cb70cc33a Gemini 텍스트 호출로 컷별 댓글 추천 구현
correct.py의 텍스트 호출 패턴을 따라 ai_pick_cuts() 및 헬퍼 함수(_candidate_lines, _cut_lines, _parse_ai)를 추가했다.
- 목차 댓글 필터링(MAX_TIMES 기준)
- 좋아요순 정렬
- 응답 파싱 시 형식 오류는 조용히 버림
- 모든 실패(키 없음, 타임아웃, JSON 깨짐 등)에 {} 반환

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:56:08 +09:00
8367219b7f 컷별 댓글 추천 프롬프트 파일 로더 구현
- RECOMMEND_PATH 상수 추가 (프롬프트/댓글_추천.md)
- DEFAULT_RECOMMEND 기본 프롬프트 추가 ({cuts}, {comments}, {k} 치환자 포함)
- load_recommend() 함수 구현 (없으면 기본값으로 생성 후 읽기, load_step1()과 같은 패턴)

사용자가 메모장에서 프롬프트를 직접 수정할 수 있도록 설계.
Task 4에서 .format(cuts=…, comments=…, k=…)으로 채워져 Gemini에 전달됨.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:50:12 +09:00
df8bc7baec 컷별 댓글 배정 순수 로직 — quotas_for, build_cut_picks
배정 규칙(타임스탐프 우선·중복 제거·Gemini 폴백)을 댓글 수집·HTTP 처리와
분리해 순수 함수로 구현. 기준 카드 길이 3초(CARD_SEC)로 컷별 쿼터 계산.
타임스탠프 매칭은 match_ranges() 이용, Gemini 없이 검증 가능.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:44:59 +09:00
47362a814e match_slots() 함수 구현 — 구간 슬롯 배정 기능
통짜 모드에서 영상 구간을 n개 슬롯으로 나누고 각 슬롯에 댓글을 배정하는 순수 함수를 추가했다.
- match_ranges()를 활용해 슬롯 시간대와 매칭되는 댓글을 찾음
- 좋아요 높은 댓글부터 슬롯에 배정 (슬롯당 최대 1개)
- 한 댓글이 여러 슬롯에 중복되지 않도록 관리
- exclude 집합으로 사전에 제외된 댓글 처리

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:39:39 +09:00
2d12bd0fac docs: 계획의 Global Constraints 를 git 초기화 반영해 갱신
git 저장소가 아니라 커밋 단계가 없다고 적혀 있었으나, 저장소를 만들었으므로
태스크마다 커밋하도록 바꾼다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:36:40 +09:00
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