문서 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>
This commit is contained in:
hehihoho3@gmail.com 2026-08-04 14:16:45 +09:00
parent c6092a93ac
commit 754692a343
3 changed files with 54 additions and 21 deletions

View File

@ -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 연동해 완전 자동화 아이디어 있음)

View File

@ -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 | 표시만 깨짐. 로직·결과와 무관 |
---

View File

@ -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));
}
}
```
`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: 문법 검증**