diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6ded5f3..a5f5ffa 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -231,11 +231,16 @@ title_top 서브제목 / title_main 메인제목 / channel 출처 / effect 효 **그 컷 구간 안에서** 균등 배치한다(한 컷이 덜 차도 다음 컷 카드가 앞으로 밀리지 않음). 파일/유튜브 탭·붙여넣기 탭 직접 사용은 컷 소속을 몰라 기존 `_load_comment_cards` 전체 균등 배치 그대로 쓴다. + - 컷당 장수 상한은 `max(1, floor(컷길이/3초))` — `_load_comment_cards`와 같은 규칙. + 초과분은 버린다(카드가 1초씩 번쩍이느니 몇 장 빼는 게 낫다). - ⚠ **카드 시간은 서버가 미리 확정하지 않는다.** `/auto/build`는 "몇 번 컷 소속"만 넘기고, - 파이프라인이 컷 누적 위치(`placements`)로 시간을 계산한다. 무음 제거를 켜면 타임라인이 - 압축되는데(`timeline_dur = sum(keep)`), 서버가 시간을 미리 박아두면 자막만 재매핑되고 - 카드는 혼자 어긋나기 때문이다 — 카드도 자막과 **같은 `_remap_caps()`**로 재매핑된다 - (튜플 모양이 `(start, end, path)`로 같아서 가능). 이 순서를 뒤집지 말 것. + 파이프라인이 컷 누적 위치(`placements`)로 시간을 계산한다. + - ⚠ **카드 시간 계산은 무음 제거 *뒤*다.** 자막은 `_remap_caps()`로 시간을 옮기지만 + (발화 시각을 따라가야 하니까), 카드는 **구간 자체**를 `_remap_placements()`로 옮기고 + 그 안에서 나눈다. 카드 시간을 압축 전에 만들어 자막처럼 재매핑하면 + `cards_fixed`(정확히 3초)가 2초로 눌리고, quota는 원본 길이로 잡혀 있어서 + 3초 하한도 사라진다(무음이 절반인 24초 컷 → 8장 → 압축 12초 → 장당 1.5초). + 이 순서를 뒤집지 말 것. - 렌더: comment 트랙에 **scale 0.89 / X 0**, 세로는 **윗변이 영상 바로 아래**에 오도록 카드마다 계산(§9). - 출처: 사용자가 h-lab(https://h-lab.tolag.shop/comment-cards)에서 실제 유튜브 댓글을 카드 PNG로 저장해 폴더에 넣음. (향후: h-lab API 연동해 완전 자동화 아이디어 있음) diff --git a/SETUP.md b/SETUP.md index 7e947f7..c1c3a1b 100644 --- a/SETUP.md +++ b/SETUP.md @@ -280,8 +280,10 @@ yt-dlp --version | 드래프트 열면 영상이 흰띠·댓글 위로 삐짐 | CapCut 편집 중 render_index 꼬임 | 최신 `draft.py`는 오버레이/제목 트랙 자동 잠금으로 예방 | | **클립을 옮긴 뒤 확대하면 그 클립만 템플릿 밖으로 삐짐** | CapCut이 옮긴 클립에 렌더순서를 새로(맨 위로) 매김 — 잠금으로 못 막음 | **캡컷에서 그 프로젝트를 닫고** → 웹 UI 하단 **"🩹 레이어 수리"** → 드래프트 선택 → 실행 → 캡컷에서 다시 열기 | | **영상 중간에 초록 화면이 몇 초 나옴** | yt-dlp가 키프레임 아닌 위치에서 잘라 참조 프레임이 없음 | 최신 `youtube.py`가 컷마다 자동 검증→재다운로드→정밀 재컷. 로그에 `🩹` 표시. 예전에 받은 영상은 다시 만들어야 함 | -| 댓글이 엉뚱한 장면에 뜬다 | 컷별 추천이 실패해 기존 전체 균등 배치로 폴백 | 검토 화면 상단 경고 확인. Gemini 한도 초과면 잠시 뒤 재시도 | -| 컷 섹션이 안 보이고 ⭐ 하나만 뜬다 | 추천 실패 폴백 | 위와 동일. `프롬프트/댓글_추천.md`를 고쳤다면 `{cuts}` `{comments}` `{k}` 가 남아 있는지 확인 | +| 컷 섹션에 🤖 배지가 하나도 없다 (⭐만 있거나 비어 있음) | Gemini 추천 실패 — 한도 초과(429)·타임아웃·응답 파싱. 컷 섹션 자체는 타임스탬프만으로 그대로 뜬다 | 분석 로그(진행 화면 아래)에 `⚠️ ID n AI 추천 실패 — 타임스탬프만으로 배정했습니다` 가 있는지 확인. 한도 초과면 잠시 뒤 재분석. `프롬프트/댓글_추천.md`를 고쳤다면 `{cuts}` `{comments}` `{k}` 가 남아 있는지 확인 | +| 컷 섹션이 아예 안 보이고 ⭐ + ➕ 두 덩어리만 뜬다 | 편집안에 컷 정보가 없어(`hl.cuts` 없음) 예전 화면으로 폴백 | 편집안 생성(Step 3)이 실패한 ID다. 로그에서 그 ID의 실패 사유 확인. 이 화면에서 고른 카드는 컷 배치 없이 전체 균등으로 깔린다 | +| 댓글이 엉뚱한 장면에 뜬다 | 컷 소속 없이 전체 균등 배치로 깔림(위 폴백 화면) 또는 추천 자체가 안 맞음 | 위 두 줄 확인. 컷 섹션이 보인다면 그 컷 섹션 안에서 카드를 갈아끼우면 그 컷 위로 옮겨진다 | +| "컷별 댓글 추천"에서 오래 멈춰 보인다 | 하이라이트마다 Gemini 를 순차로 부른다(429 회피). 최악 5×90초 | **새로고침하지 말 것** — 분석이 통째로 날아간다. 로그에 `ID n 컷별 댓글 추천 중… (i/N)` 이 올라오면 정상 진행 중 | | 콘솔에 한글 깨짐 | Windows cp949 | 표시만 깨짐. 로직·결과와 무관 | --- diff --git a/docs/superpowers/plans/2026-08-04-컷별-댓글-추천.md b/docs/superpowers/plans/2026-08-04-컷별-댓글-추천.md index 5c52695..c1554b3 100644 --- a/docs/superpowers/plans/2026-08-04-컷별-댓글-추천.md +++ b/docs/superpowers/plans/2026-08-04-컷별-댓글-추천.md @@ -1123,36 +1123,56 @@ function toggle(hlId,idx,ci){ // 영상 편집안(컷 목록·JSON) — 선택 요약 바 위에, 기본 접힘 box.insertBefore(cutsSection(hl),$("#hlsel-"+hl.id)); const WHY={ts:"⭐",ai:"🤖",like:"➕"}; + const cand=(hl.candidates||[]).filter(i=>byIdx[i]!==undefined); if(hl.cuts){ - // 컷별 추천 — 추천분 먼저, 그 컷 시간대 언급 댓글을 뒤에 붙여 갈아끼울 수 있게 + // 컷별 추천 — 컷마다 두 섹션(둘 다 ci=cu.i). 독립 ➕ 섹션을 두지 않는다(아래 설명). for(const cu of hl.cuts){ const rec=(cu.picks||[]).map(p=>p.idx).filter(i=>byIdx[i]!==undefined); const why=(cu.picks||[]).map(p=>WHY[p.why]||"").join(""); - const rest=(hl.matched||[]).filter(i=>byIdx[i]!==undefined&&!rec.includes(i)); + // '나머지'는 그 컷 시간대를 언급한 댓글만 — matched 전체를 컷마다 복제하면 + // DOM 이 컷 수만큼 부풀고 같은 댓글이 여러 섹션에 중복 표시된다. + const cs=(hl.paste.cuts||[])[cu.i]||{start:0,end:-1}; + const rest=(hl.matched||[]).filter(i=> + byIdx[i]!==undefined&&!rec.includes(i)&& + (byIdx[i].times||[]).some(t=>t>=cs.start&&t<=cs.end)); const label="컷 "+(cu.i+1)+" · "+cu.sec+"초 · 카드 "+cu.quota+"장"+ (cu.bottom?" — "+cu.bottom:"")+(why?" "+why:""); box.appendChild(cardSection(label,rec.concat(rest),hl.id, Math.max(CARD_PAGE,rec.length),cu.i)); + // 부족분 채우기 — 컷마다 반복되므로 초기 렌더는 6장(CUT_FILL_PAGE)만 + if(cand.length) + box.appendChild(cardSection( + "↳ 좋아요 상위에서 채우기 "+cand.length+"장 (이 컷에 넣기)", + cand,hl.id,CUT_FILL_PAGE,cu.i)); } }else{ - // 폴백 — 추천 실패 시 기존 화면 그대로 + // 폴백 — hl.cuts 가 없으면 예전 화면 그대로: ⭐ + 독립 ➕ const m=(hl.matched||[]).filter(i=>byIdx[i]!==undefined); box.appendChild(cardSection( "⭐ 이 구간을 언급한 댓글 "+m.length+"장 (좋아요순, 자동 선택)", m,hl.id,Math.max(CARD_PAGE,sel[hl.id].length))); - } - // ➕ 좋아요 상위 후보 (전체 — 30장씩 더보기) - const cand=(hl.candidates||[]).filter(i=>byIdx[i]!==undefined); - if(cand.length){ - box.appendChild(cardSection( - "➕ 좋아요 상위 후보 "+cand.length+"장 (부족분 클릭)", - cand,hl.id,CARD_PAGE)); + if(cand.length){ + box.appendChild(cardSection( + "➕ 좋아요 상위 후보 "+cand.length+"장 (부족분 클릭)", + cand,hl.id,CARD_PAGE)); + } } ``` -`cardSection`의 `ci`가 `undefined`인 섹션(➕ 후보, 폴백)에서 카드를 누르면 `toggle`이 -컷 없이 동작한다. 이 경우 `sortSel`이 그 카드를 맨 뒤로 보낸다(`??999`) — 마지막 컷 뒤에 -붙는다는 뜻이고, 배치상 문제 없다. +⚠ **왜 독립 ➕ 섹션을 없앴나** (초안의 판단이 틀렸다). + +초안은 "`ci`가 `undefined`면 `sortSel`이 그 카드를 맨 뒤로 보내니(`??999`) 마지막 컷 +**뒤에** 붙는다 — 배치상 문제 없다"고 적었다. **틀렸다.** `_cards_by_cut`은 카드를 컷 +뒤에 붙이지 않고 **컷 안에서 균등 분할**한다. `card_cuts`에 마지막 컷 인덱스가 들어가면 +그 컷 구간 안으로 전부 밀려 들어가, 4초짜리 마지막 컷에 5장이면 0.8초씩 번쩍인다. + +게다가 이건 드문 경로가 아니다. 추천 프롬프트가 "억지로 채우지 마라, 어울리는 게 없으면 +빈 배열로 둬라"라고 지시하므로 **quota 미달이 기본 동작**이고, 스펙 §7은 그 미달을 +"사람이 ➕에서 채운다"로 풀게 했다 — 즉 주 워크플로다. + +그래서 `hl.cuts`가 있는 경로에서는 ➕를 **컷 섹션 안으로 흡수**한다(위 코드). +`hl.cuts`가 없는 폴백 경로만 예전처럼 ⭐ + 독립 ➕를 쓴다 — 거기선 애초에 컷 소속 개념이 +없고 `card_cuts`도 안 보낸다. - [ ] **Step 4: 빌드 전송에 `card_cuts` 추가** @@ -1169,18 +1189,24 @@ function toggle(hlId,idx,ci){ for(const idx of sel[hl.id]){ const w=wrapOf(hl.id,idx); if(!w) continue; + const ci=cmap[idx]; + // 컷 소속을 모르는 카드는 건너뛴다 — 엉뚱한 컷에 몰아넣지 않는다 + if(hl.cuts&&ci===undefined) continue; boardSet(hl.id,null,"카드 캡처 중… "+(++n)+"/"+sel[hl.id].length,"active"); try{ const blob=await captureCard(w); + // ⚠ append 와 push 는 반드시 쌍으로 fd.append("cards",blob,String(n).padStart(3,"0")+".png"); - sentCuts.push(cmap[idx]??((hl.paste.cuts||[]).length-1)); + sentCuts.push(ci); }catch(e){n--;boardSet(hl.id,null,"카드 1장 캡처 실패(건너뜀)","active");} } if(hl.cuts) fd.append("card_cuts",JSON.stringify(sentCuts)); ``` 캡처 실패 시 `n--`를 하는 이유: 파일명 번호(`001.png`)와 `sentCuts` 인덱스가 어긋나면 -카드가 엉뚱한 컷에 붙는다. +카드가 엉뚱한 컷에 붙는다. 같은 이유로 `fd.append("cards", …)`와 `sentCuts.push(…)`는 +**반드시 쌍으로** 돈다 — 하나만 실행되는 경로를 만들면 그 뒤 카드가 전부 한 칸씩 밀린다. +컷 소속을 모르는 카드(`ci === undefined`)는 마지막 컷으로 폴백하지 않고 **둘 다 건너뛴다**. - [ ] **Step 5: 문법 검증**