From 5b58e94ad1bec968de53a1d656fd6511cb8489f2 Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Tue, 4 Aug 2026 12:28:11 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B2=80=ED=86=A0=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=9D=84=20=EC=BB=B7=EB=B3=84=20=EC=B9=B4=EB=93=9C=20=EB=AC=B6?= =?UTF-8?q?=EC=9D=8C=EC=9C=BC=EB=A1=9C=20=EB=A0=8C=EB=8D=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 서버가 하이라이트마다 컷 단위 추천(hl.cuts)을 내려주기 시작해서(6~8번 커밋), 화면도 컷 소속을 알아야 카드를 그 컷 구간 안에 배치할 수 있다. 카드가 어느 컷에 속하는지(selCut)를 추적해 컷별 상한(quota)을 걸고, sel[hlId]를 항상 컷 순서로 정렬해 업로드 파일명(001.png…)과 card_cuts 배열 인덱스가 어긋나지 않게 했다. hl.cuts가 없으면(추천 실패) 기존 ⭐ 통짜 화면으로 그대로 폴백한다. Co-Authored-By: Claude Opus 5 (1M context) --- server/static/auto.js | 78 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/server/static/auto.js b/server/static/auto.js index a7eadc1..c75b31a 100644 --- a/server/static/auto.js +++ b/server/static/auto.js @@ -27,7 +27,7 @@ function hlById(id){ } /* ── 카드 DOM (h-lab renderCards 구조와 동일) ── */ -function cardEl(c,hlId){ +function cardEl(c,hlId,ci){ const wrap=document.createElement("div"); wrap.className="ccwrap"; wrap.dataset.cidx=c.idx; const card=document.createElement("div"); @@ -51,7 +51,7 @@ function cardEl(c,hlId){ const badge=document.createElement("div"); badge.className="ccbadge"; badge.textContent="✓"; wrap.appendChild(card); wrap.appendChild(badge); - wrap.addEventListener("click",()=>toggle(hlId,c.idx)); + wrap.addEventListener("click",()=>toggle(hlId,c.idx,ci)); return wrap; } @@ -61,7 +61,7 @@ function wrapOf(hlId,idx){ /* ── 카드 섹션 — 30장씩 렌더 + 더보기 (전체 댓글을 받아도 DOM은 점진 생성) ── */ const CARD_PAGE=30; -function cardSection(label,list,hlId,firstBatch){ +function cardSection(label,list,hlId,firstBatch,ci){ const sec=document.createElement("div");sec.className="hlsec"; const head=document.createElement("div");head.textContent=label; sec.appendChild(head); @@ -75,7 +75,7 @@ function cardSection(label,list,hlId,firstBatch){ for(;shown(m[a]??999)-(m[b]??999)); +} +function cutFull(hl,ci){ + const m=selCut[hl.id]||{}; + const q=(hl.cuts&&hl.cuts[ci])?hl.cuts[ci].quota:hl.need; + return sel[hl.id].filter(i=>m[i]===ci).length>=q; +} +function toggle(hlId,idx,ci){ const hl=hlById(hlId), list=sel[hlId]; + selCut[hlId]=selCut[hlId]||{}; const at=list.indexOf(idx); - if(at>=0) list.splice(at,1); + if(at>=0){ list.splice(at,1); delete selCut[hlId][idx]; } else{ - if(list.length>=hl.need) return; // 필요 장수 초과 선택 방지 + if(hl.cuts&&ci!==undefined){ + if(cutFull(hl,ci)) return; // 그 컷 장수 초과 방지 + selCut[hlId][idx]=ci; + }else{ + if(list.length>=hl.need) return; // 폴백(컷 정보 없음) + } list.push(idx); + sortSel(hlId); } refreshSel(hlId); } @@ -435,7 +456,17 @@ function onResult(ev){ // 패널 const box=document.createElement("div"); box.className="hlbox";box.id="hlbox-"+hl.id; - sel[hl.id]=(hl.matched||[]).slice(0,hl.need).filter(i=>byIdx[i]!==undefined); + // 자동 선택: 컷별 추천을 컷 순서대로. hl.cuts 가 없으면 기존 방식으로 폴백. + sel[hl.id]=[]; selCut[hl.id]={}; + if(hl.cuts){ + for(const cu of hl.cuts) + for(const p of (cu.picks||[])) + if(byIdx[p.idx]!==undefined&&!sel[hl.id].includes(p.idx)){ + sel[hl.id].push(p.idx); selCut[hl.id][p.idx]=cu.i; + } + }else{ + sel[hl.id]=(hl.matched||[]).slice(0,hl.need).filter(i=>byIdx[i]!==undefined); + } const cuts=hl.paste.cuts; let html="

ID "+hl.id+" · "+fmtT(hl.start)+"~"+fmtT(hl.end)+ " "+esc(hl.reason||"")+"

"+ @@ -458,11 +489,25 @@ function onResult(ev){ box.dataset.titles=JSON.stringify(opts); // 영상 편집안(컷 목록·JSON) — 선택 요약 바 위에, 기본 접힘 box.insertBefore(cutsSection(hl),$("#hlsel-"+hl.id)); - // ⭐ 구간 언급 댓글 (전체 — 30장씩 더보기) - 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))); // 자동 선택분은 첫 화면에 다 보이게 + const WHY={ts:"⭐",ai:"🤖",like:"➕"}; + if(hl.cuts){ + // 컷별 추천 — 추천분 먼저, 그 컷 시간대 언급 댓글을 뒤에 붙여 갈아끼울 수 있게 + 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)); + 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)); + } + }else{ + // 폴백 — 추천 실패 시 기존 화면 그대로 + 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){ @@ -551,7 +596,8 @@ async function buildAll(){ fd.append("remove_silence",$("#rmsilence").checked?"1":"0"); fd.append("asr_bottom",$("#asrbottom").checked?"1":"0"); fd.append("cards_fixed",($("#autoCardsFixed")&&$("#autoCardsFixed").checked)?"1":"0"); - let n=0; + const cmap=selCut[hl.id]||{}; + let n=0; const sentCuts=[]; for(const idx of sel[hl.id]){ const w=wrapOf(hl.id,idx); if(!w) continue; @@ -559,8 +605,10 @@ async function buildAll(){ try{ const blob=await captureCard(w); fd.append("cards",blob,String(n).padStart(3,"0")+".png"); - }catch(e){boardSet(hl.id,null,"카드 1장 캡처 실패(건너뜀)","active");} + sentCuts.push(cmap[idx]??((hl.paste.cuts||[]).length-1)); + }catch(e){n--;boardSet(hl.id,null,"카드 1장 캡처 실패(건너뜀)","active");} } + if(hl.cuts) fd.append("card_cuts",JSON.stringify(sentCuts)); boardSet(hl.id,null,"빌드 요청 중…","active"); const res=await(await fetch("/auto/build",{method:"POST",body:fd})).json(); if(res.error) throw new Error(res.error);