capcut-agent/server/static/index.html
hehihoho3@gmail.com 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

868 lines
54 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>캡컷 에이전트 · 구간합치기</title>
<style>
:root{
/* 편집실 팔레트 — 순흑이 아니라 편집 프로그램의 청회색 레이어 */
--bg:#0B0D12; --card:#12151D; --border:#262D3A; --hr:#1C212C;
--surf:#161B25; /* 버튼·칩·탭 표면 (구 #141414/#1c1c1c) */
--surf2:#0D1017; /* 입력면 (구 #121212/#0f0f0f) */
--acc-soft:#0E2320; /* 활성 배경 (구 var(--acc-soft)) */
--text:#EAECF1; --muted:#7E8899; --muted2:#A2ACBD;
--accent:#2CE0C9; /* CapCut 틸 — 이 도구의 종착지 */
--accent-ink:#03211C; /* 틸 버튼 위 글자 */
--rec:#FF5D55; /* 재생헤드 · REC */
--danger:#F0716B;
--mono:ui-monospace,"SF Mono","JetBrains Mono","Cascadia Code",monospace;
--sans:"Pretendard Variable",Pretendard,system-ui,-apple-system,"Segoe UI",sans-serif;
}
*{box-sizing:border-box;} html,body{margin:0;height:100%;}
body{background:var(--bg);color:var(--text);font-family:var(--sans);font-size:14px;line-height:1.5;
-webkit-font-smoothing:antialiased;display:flex;justify-content:center;padding:48px 20px 120px;
background-image:radial-gradient(900px 340px at 50% -80px,rgba(44,224,201,.045),transparent 70%);}
::selection{background:rgba(44,224,201,.25);}
:is(button,input,select,textarea,summary,a):focus-visible{
outline:2px solid var(--accent);outline-offset:2px;border-radius:4px;}
::-webkit-scrollbar{width:10px;height:10px;}
::-webkit-scrollbar-thumb{background:#2A3140;border-radius:5px;border:2px solid var(--bg);}
::-webkit-scrollbar-track{background:transparent;}
.wrap{width:100%;max-width:600px;}
.tnum{font-variant-numeric:tabular-nums;}
header{margin-bottom:22px;}
.brand{display:flex;align-items:center;gap:9px;}
.dot{width:8px;height:8px;border-radius:50%;background:var(--rec);
box-shadow:0 0 8px rgba(255,93,85,.7);flex:none;animation:recblink 2.4s ease-in-out infinite;}
@keyframes recblink{0%,100%{opacity:1;}50%{opacity:.45;}}
h1{font-size:16px;font-weight:700;margin:0;letter-spacing:-0.02em;}
.sub{color:var(--muted);font-family:var(--mono);font-size:11.5px;margin:8px 0 0 17px;letter-spacing:.02em;}
/* ── 시그니처: 필름 타임라인 스트립 — 클립 조각 + 흐르는 재생헤드 ── */
.tl{position:relative;display:flex;gap:4px;align-items:center;height:10px;
margin:14px 0 0;overflow:hidden;border-radius:3px;}
.tl .tlc{display:block;height:100%;border-radius:2px;background:#222A38;flex:none;}
.tl .tlc.on{background:linear-gradient(90deg,rgba(44,224,201,.85),rgba(44,224,201,.5));}
.tl .tlph{position:absolute;top:-2px;bottom:-2px;left:0;width:2px;background:var(--rec);
box-shadow:0 0 6px rgba(255,93,85,.8);animation:phdrift 26s ease-in-out infinite alternate;}
@keyframes phdrift{from{transform:translateX(0);}to{transform:translateX(min(596px,calc(100vw - 44px)));}}
.card{background:var(--card);border:1px solid var(--border);border-radius:12px;}
@media (prefers-reduced-motion:reduce){
*,*::before,*::after{animation:none !important;transition:none !important;}
}
/* tabs */
.tabs{display:flex;gap:6px;margin-bottom:14px;}
.tab{flex:1;background:var(--surf);border:1px solid var(--border);color:var(--muted2);
border-radius:9px;padding:11px 10px;min-height:44px;font-family:var(--sans);font-size:13px;font-weight:500;
cursor:pointer;transition:.15s;}
.tab:hover{border-color:#39424F;color:var(--text);}
.tab.active{background:var(--acc-soft);border-color:var(--accent);color:var(--text);font-weight:600;}
#drop{border:1px dashed var(--border);border-radius:10px;background:var(--card);
padding:32px 20px;text-align:center;cursor:pointer;transition:border-color .15s,background .15s;}
#drop:hover{border-color:#39424F;} #drop.drag{border-color:var(--accent);border-style:solid;}
#drop .big{font-size:14px;} #drop .hint{color:var(--muted);font-family:var(--mono);font-size:12px;margin-top:8px;}
input[type=file]{display:none;}
.chip{font-family:var(--mono);font-size:12px;color:var(--muted2);background:var(--surf);border:1px solid var(--border);
border-radius:6px;padding:5px 9px;display:inline-flex;align-items:center;gap:7px;overflow:hidden;
white-space:nowrap;text-overflow:ellipsis;max-width:100%;}
.chip .k{color:var(--muted);}
.filerow{margin-top:12px;}
.fields{display:grid;gap:8px;margin-top:14px;}
.field label{display:block;color:var(--muted);font-size:11.5px;margin-bottom:4px;letter-spacing:.02em;}
.field input{width:100%;background:var(--surf2);border:1px solid var(--border);border-radius:7px;color:var(--text);
font-family:var(--sans);font-size:13px;padding:9px 11px;outline:none;}
.field input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(44,224,201,.10);}
.field input::placeholder{color:#495364;}
.field.mono input{font-family:var(--mono);}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:8px;}
.note{color:var(--muted);font-family:var(--mono);font-size:11px;margin-top:8px;}
button.run{font-family:var(--sans);font-size:13.5px;font-weight:700;color:var(--accent-ink);background:var(--accent);
border:0;border-radius:9px;padding:12px 16px;min-height:46px;cursor:pointer;width:100%;margin-top:14px;
transition:filter .15s,transform .1s;}
button.run:hover{filter:brightness(1.08);} button.run:active{transform:scale(.985);}
button.run:disabled{opacity:.4;cursor:default;transform:none;}
#log{display:none;margin-top:16px;font-family:var(--mono);font-size:11.5px;color:var(--muted);line-height:1.8;}
#log .line:before{content:" ";color:#39424F;}
#steps{display:none;margin-top:16px;}
.step{display:flex;align-items:flex-start;gap:11px;padding:11px 0;}
.step+.step{border-top:1px solid var(--hr);}
.sdot{width:4px;height:16px;border-radius:2px;background:#29303D;margin-top:2px;flex:none;transition:background .2s;}
.step.active .sdot{background:var(--accent);box-shadow:0 0 8px var(--accent);animation:pulse 1.1s ease-in-out infinite;}
.step.done .sdot{background:var(--accent);} .step.err .sdot{background:var(--danger);box-shadow:0 0 8px var(--danger);}
@keyframes pulse{0%,100%{opacity:1;}50%{opacity:.35;}}
.slabel{flex:1;min-width:0;} .slabel .t{font-size:13.5px;color:var(--muted2);}
.step.active .slabel .t,.step.done .slabel .t{color:var(--text);}
.sdetail{color:var(--muted);font-family:var(--mono);font-size:12px;margin-top:3px;word-break:break-all;}
.step.err .sdetail{color:var(--danger);}
.selapsed{color:var(--muted);font-family:var(--mono);font-size:12px;flex:none;margin-top:1px;}
#result{display:none;margin-top:18px;}
.grid3{display:grid;grid-template-columns:repeat(3,1fr);}
.stat+.stat{border-left:1px solid var(--hr);padding-left:16px;} .stat:not(:first-child){padding-left:16px;}
.stat .v{font-family:var(--mono);font-size:22px;font-weight:500;letter-spacing:-.02em;}
.stat .v.green{color:var(--accent);} .stat .l{color:var(--muted);font-size:11.5px;margin-top:4px;}
hr.sep{border:0;border-top:1px solid var(--hr);margin:16px 0;}
.reslabel{color:var(--muted);font-size:11.5px;margin-bottom:8px;}
.opennote{color:var(--muted);font-family:var(--mono);font-size:11.5px;margin-top:14px;line-height:1.7;}
.opennote b{color:var(--muted2);font-weight:500;}
button.capcut{margin-top:14px;width:100%;font-family:var(--sans);font-size:13.5px;font-weight:700;color:var(--accent-ink);
background:var(--accent);border:0;border-radius:9px;padding:12px;min-height:46px;cursor:pointer;
transition:filter .15s,transform .1s;}
button.capcut:hover{filter:brightness(1.08);} button.capcut:active{transform:scale(.985);}
.ghost{margin-top:8px;width:100%;background:var(--surf);border:1px solid var(--border);color:var(--muted2);
border-radius:8px;padding:9px;font-family:var(--sans);font-size:12.5px;cursor:pointer;}
.ghost:hover{color:var(--text);border-color:#39424F;}
.hdrlinks{display:flex;gap:8px;flex-wrap:wrap;}
.hlink{display:inline-flex;align-items:center;gap:5px;text-decoration:none;color:var(--muted2);
background:var(--card);border:1px solid var(--border);border-radius:999px;padding:5px 12px;
font-size:12px;font-family:var(--sans);white-space:nowrap;transition:color .12s,border-color .12s;}
.hlink:hover{color:var(--accent);border-color:var(--accent);}
.helprow{display:flex;justify-content:flex-end;margin:0 0 6px;}
.helpbtn{background:transparent;color:var(--muted2);border:1px solid var(--border);
border-radius:999px;padding:3px 11px;font-size:11.5px;cursor:pointer;font-family:var(--sans);}
.helpbtn:hover{color:var(--accent);border-color:var(--accent);}
#helpOverlay{position:fixed;inset:0;background:rgba(0,0,0,.65);display:none;
align-items:center;justify-content:center;z-index:50;padding:20px;}
#helpModal{background:var(--card);border:1px solid var(--border);border-radius:14px;
max-width:560px;width:100%;max-height:80vh;overflow-y:auto;padding:22px 24px;position:relative;}
#helpModal h3{margin:0 0 12px;font-size:15px;color:var(--text);}
#helpModal ol,#helpModal ul{margin:8px 0;padding-left:20px;color:var(--muted2);font-size:13px;line-height:1.75;}
#helpModal li b{color:var(--text);font-weight:600;}
#helpModal code{background:var(--surf2);border:1px solid var(--border);border-radius:5px;
padding:1px 6px;font-family:var(--mono);font-size:11.5px;color:var(--accent);}
#helpModal pre{background:var(--surf2);border:1px solid var(--border);border-radius:8px;
padding:10px 12px;font-family:var(--mono);font-size:11px;line-height:1.55;overflow-x:auto;color:var(--muted2);margin:8px 0;}
#helpClose{position:absolute;top:12px;right:14px;background:transparent;border:0;
color:var(--muted2);font-size:16px;cursor:pointer;}
#helpClose:hover{color:var(--text);}
.helpdim{color:var(--muted);font-size:12px;margin-top:10px;line-height:1.6;}
/* ── 자동 탭: 댓글 카드 (h-lab comment-cards 이식 · 검정배경/둥근모서리/모자이크 고정) ── */
.ccwrap{position:relative;display:inline-block;width:320px;margin:0 10px 10px 0;
vertical-align:top;cursor:pointer;border-radius:18px;}
.ccwrap.sel{outline:2px solid var(--accent);outline-offset:2px;}
/* 다른 ID(탭)에서 이미 선택한 댓글 — 주황 점선 + 배지 (자기 탭 선택 초록이 우선) */
.ccwrap.used:not(.sel){outline:2px dashed #d97706;outline-offset:2px;}
.ccwrap .ccused{position:absolute;top:-8px;left:8px;background:#d97706;color:#000;
font-size:10.5px;font-weight:700;padding:2px 8px;border-radius:999px;z-index:2;display:none;}
.ccwrap.used .ccused{display:block;}
.ccwrap .ccbadge{position:absolute;top:-7px;right:-7px;width:22px;height:22px;border-radius:50%;
background:var(--accent);color:#000;font-size:13px;font-weight:700;display:none;
align-items:center;justify-content:center;z-index:2;}
.ccwrap.sel .ccbadge{display:flex;}
.comment-card{background:#000;border:1px solid #333;border-radius:16px;padding:16px;
box-sizing:border-box;width:320px;
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Malgun Gothic",sans-serif;
-webkit-font-smoothing:antialiased;}
.cc-head{display:flex;gap:9.6px;align-items:flex-start;}
.cc-avatar{width:40px;height:40px;border-radius:50%;flex-shrink:0;object-fit:cover;background:#222;}
.cc-meta{flex:1;min-width:0;}
.cc-author{font-weight:600;font-size:13.5px;color:#fff;}
.cc-time{font-size:11.5px;color:#aaa;margin-left:6.4px;}
.cc-text{font-size:14px;color:#fff;margin-top:5.6px;white-space:pre-wrap;word-break:break-word;line-height:1.5;}
.cc-stats{font-size:12px;color:#aaa;margin-top:8px;display:flex;gap:16px;font-variant-numeric:tabular-nums;}
.comment-card.mosaic .cc-avatar{filter:blur(6px);}
.comment-card.mosaic .cc-author{filter:blur(5px);}
/* 검토 화면 — 본문 600px 유지, 검토 영역만 와이드 브레이크아웃(댓글 카드 3~4열) */
#autoReview,#autoSummary,#ytccArea{--rw:min(1240px,calc(100vw - 40px));
width:var(--rw);margin-left:calc((100% - var(--rw))/2);}
/* ID 탭 바 — 스크롤해도 위에 붙어 있음 */
.idtabs{position:sticky;top:0;z-index:40;display:flex;gap:6px;flex-wrap:wrap;
padding:10px 0;background:var(--bg);margin-top:10px;}
.idtab{flex:1;min-width:120px;min-height:46px;background:var(--surf);border:1px solid var(--border);
border-radius:8px;color:var(--muted2);cursor:pointer;padding:7px 11px;text-align:left;
font-family:var(--sans);transition:.15s;}
.idtab:hover{border-color:#39424F;}
.idtab.active{background:var(--acc-soft);border-color:var(--accent);color:var(--text);}
.idtab.err{border-color:var(--danger);color:var(--danger);cursor:default;}
.idtab .tt{font-size:12.5px;font-weight:600;display:block;}
.idtab .ts{font-size:11px;font-family:var(--mono);display:block;margin-top:2px;color:var(--muted);}
.idtab.full .ts{color:var(--accent);}
/* ID 제외(✕) — .idtab 이 이미 <button> 이라 그 안에 버튼을 못 넣는다(중첩 button 금지).
래퍼로 감싸고 ✕ 를 형제로 두어 겹쳐 놓는다. 자리는 항상 확보(hover 시 레이아웃 안 흔들림) */
.idtabwrap{position:relative;flex:1;min-width:120px;display:flex;}
.idtabwrap .idtab{flex:1;min-width:0;padding-right:36px;}
.xdel{position:absolute;top:50%;right:5px;transform:translateY(-50%);
width:28px;height:28px;display:none;align-items:center;justify-content:center;
border:0;border-radius:7px;background:transparent;color:var(--muted);
font-size:13px;line-height:1;cursor:pointer;transition:background .15s,color .15s;}
.idtabwrap:hover .xdel,.idtab.active~.xdel,.xdel:focus-visible{display:flex;}
.xdel:hover{background:var(--danger);color:#fff;}
.idtab.dropped{opacity:.55;border-style:dashed;background:transparent;}
.idtab.dropped .tt{text-decoration:line-through;}
.idtab.dropped .ts{color:var(--danger);}
.idtab.dropped~.xdel{display:flex;color:var(--accent);font-size:15px;}
.idtab.dropped~.xdel:hover{background:var(--acc-soft);color:var(--accent);}
.hlbox{display:none;margin-top:4px;padding:16px;background:var(--card);border:1px solid var(--border);border-radius:10px;}
.hlbox.show{display:block;}
.hlbox h3{margin:0 0 6px;font-size:14.5px;}
.hlbox .hlmeta{color:var(--muted2);font-size:12.5px;margin-bottom:8px;}
.hlbox .hlsec{margin-top:12px;color:var(--muted2);font-size:12.5px;}
.hlbox select{background:var(--surf2);border:1px solid var(--border);border-radius:7px;
color:var(--text);font-size:12.5px;padding:8px 10px;max-width:100%;}
.hlbox .hlprog{margin-top:8px;font-family:var(--mono);font-size:12px;color:var(--muted2);white-space:pre-line;}
/* ── 영상 편집안(JSON 컷) — 기본 접힘, 클릭해 펼침 ── */
.cutsbox{margin-top:12px;border:1px solid var(--border);border-radius:9px;
background:var(--surf2);overflow:hidden;}
.cutsbox>summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:8px;
padding:11px 13px;min-height:44px;box-sizing:border-box;font-size:12.5px;font-weight:600;
color:var(--muted2);transition:background .15s,color .15s;}
.cutsbox>summary::-webkit-details-marker{display:none;}
.cutsbox>summary:hover{color:var(--text);background:var(--surf);}
.cutsbox[open]>summary{color:var(--text);border-bottom:1px solid var(--border);}
.cutsbox .chev{flex:none;color:var(--muted);font-size:10px;transition:transform .18s ease;}
.cutsbox[open] .chev{transform:rotate(90deg);}
.cutsbox .cprev{color:var(--muted);font-weight:400;overflow:hidden;text-overflow:ellipsis;
white-space:nowrap;min-width:0;}
.cutsbox[open] .cprev{display:none;}
.cutsbox .cnum{margin-left:auto;flex:none;font-family:var(--mono);font-size:11.5px;
color:var(--muted);font-weight:400;}
.cutlist{max-height:340px;overflow-y:auto;}
.cutrow{display:grid;grid-template-columns:24px 104px 1fr;gap:10px;align-items:start;
padding:9px 13px;border-top:1px solid var(--hr);font-size:12.5px;}
.cutrow:first-child{border-top:0;}
.cutrow .cno{font-family:var(--mono);font-size:11.5px;color:var(--muted);text-align:right;}
.cutrow .ctime{font-family:var(--mono);font-size:11.5px;color:var(--accent);
font-variant-numeric:tabular-nums;}
.cutrow .ctime .cdur{display:block;color:var(--muted);}
.cutrow .cbody{min-width:0;}
.cutrow .cbot{color:var(--text);white-space:pre-wrap;word-break:break-word;line-height:1.55;}
.cutrow .cbot.cempty{color:var(--muted);}
.cutrow .ceff{display:block;margin-top:3px;color:#33D17A;font-size:11.5px;word-break:break-word;}
.cutraw{border-top:1px solid var(--border);}
.cutraw>summary{cursor:pointer;list-style:none;padding:9px 13px;min-height:40px;
box-sizing:border-box;font-family:var(--mono);font-size:11.5px;color:var(--muted);
display:flex;align-items:center;transition:color .15s;}
.cutraw>summary::-webkit-details-marker{display:none;}
.cutraw>summary:hover{color:var(--text);}
.cutraw pre{margin:0;padding:11px 13px;background:#080A0E;font-family:var(--mono);font-size:11px;
line-height:1.55;color:var(--muted2);max-height:280px;overflow:auto;}
.cutcopy{margin:0 13px 11px;width:auto;min-height:34px;padding:6px 12px;}
/* 선택한 댓글 요약 바 */
.selbar{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:10px;
padding:10px;background:#101010;border:1px solid var(--border);border-radius:8px;min-height:44px;}
.selbar .selbar-l{color:var(--muted2);font-size:12px;margin-right:4px;}
.selchip{display:inline-flex;align-items:center;gap:6px;background:var(--surf);border:1px solid var(--border);
border-radius:999px;padding:6px 6px 6px 11px;font-size:12px;color:var(--text);cursor:pointer;
max-width:230px;white-space:nowrap;}
.selchip:hover{border-color:var(--accent);}
.selchip .no{color:var(--accent);font-family:var(--mono);flex:none;}
.selchip .nm{overflow:hidden;text-overflow:ellipsis;}
.selchip .x{color:var(--muted2);padding:2px 6px;border-radius:999px;flex:none;font-size:13px;}
.selchip .x:hover{color:#fff;background:var(--danger);}
.selonly-label{display:inline-flex;align-items:center;gap:6px;color:var(--muted2);font-size:12px;
cursor:pointer;margin-left:auto;padding:6px;}
.hlbox.selonly .cardsec .ccwrap:not(.sel){display:none;}
/* 댓글 검색창 */
.ccsearch{display:flex;gap:6px;align-items:center;margin-top:10px;}
.ccsearch input{flex:1;max-width:420px;min-height:40px;padding:8px 12px;background:var(--surf);
border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px;}
.ccsearch input:focus{outline:none;border-color:var(--accent);}
.ccsearch .ccsx{width:auto;min-height:40px;padding:6px 13px;flex:none;margin:0;}
/* 카드 그리드 */
.cardsec{display:flex;flex-wrap:wrap;gap:12px;margin-top:8px;}
.cardsec .ccwrap{margin:0;}
.cc-more{margin-top:10px;min-height:40px;}
.hlbox.selonly .cc-more{display:none;}
/* ⚙ 지침·모델 수정 — 버튼처럼 잘 보이게 */
#autoSettings summary{cursor:pointer;list-style:none;background:var(--surf);border:1px solid var(--border);
border-radius:8px;padding:12px 14px;font-size:13px;font-weight:600;color:var(--text);
display:flex;align-items:center;gap:8px;transition:.15s;min-height:44px;box-sizing:border-box;}
#autoSettings summary::-webkit-details-marker{display:none;}
#autoSettings summary:hover{border-color:var(--accent);}
#autoSettings[open] summary{background:var(--acc-soft);border-color:var(--accent);}
#autoSettings summary .smhint{margin-left:auto;color:var(--muted);font-weight:400;
font-family:var(--mono);font-size:11.5px;}
/* 빌드 중 댓글 영역 접기 — display:none 이면 카드 캡처가 안 되므로 화면 밖으로만 밀어냄 */
#autoReview.folded .cardsec{position:absolute;left:-100000px;top:0;}
#autoReview.folded .cc-more{display:none;}
.ccfoldbtn{flex:none !important;min-width:120px;}
/* 자동 탭 방식 선택 */
.modeopt{position:relative;flex:1;min-width:150px;display:flex;flex-direction:column;gap:3px;align-items:flex-start;
background:var(--surf);border:1px solid var(--border);border-radius:9px;padding:10px 12px;min-height:52px;
font-size:13px;color:var(--muted2);cursor:pointer;transition:.15s;}
.modeopt:hover{border-color:#39424F;color:var(--text);}
.modeopt:has(input:checked){background:var(--acc-soft);border-color:var(--accent);color:var(--text);font-weight:600;}
/* 라디오는 숨기고(카드가 곧 선택 표시) 키보드 포커스 링은 카드에 */
.modeopt input{position:absolute;opacity:0;pointer-events:none;}
.modeopt:has(input:focus-visible){outline:2px solid var(--accent);outline-offset:2px;}
.modeopt .modetitle{display:block;}
.modeopt .modedesc{display:block;color:var(--muted);font-size:11px;font-family:var(--mono);font-weight:400;}
/* 통짜 모드 제목 입력 */
.hlinput{background:var(--surf2);border:1px solid var(--border);border-radius:7px;
color:var(--text);font-size:12.5px;padding:8px 10px;width:200px;max-width:45%;}
/* 빌드 진행판 — 5개 전부 만들기 아래, ID별 상태 한눈에 */
#buildBoard{display:none;margin-top:10px;}
.bbrow{display:flex;gap:10px;align-items:baseline;padding:9px 12px;border:1px solid var(--border);
border-radius:8px;background:var(--card);margin-top:6px;font-size:12.5px;}
.bbrow.active{border-color:var(--accent);}
.bbrow.err{border-color:var(--danger);}
.bbid{font-weight:600;flex:none;min-width:88px;}
.bbstat{flex:none;font-family:var(--mono);font-size:12px;color:var(--muted2);min-width:64px;}
.bbrow.active .bbstat{color:var(--accent);}
.bbrow.err .bbstat{color:var(--danger);}
.bbmsg{font-family:var(--mono);font-size:12px;color:var(--muted2);flex:1;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="brand" style="justify-content:space-between;flex-wrap:wrap;gap:10px;">
<div style="display:flex;align-items:center;gap:9px;"><span class="dot"></span><h1>캡컷 에이전트 · 구간합치기</h1></div>
<div class="hdrlinks">
<a class="hlink" href="https://aistudio.google.com/" target="_blank" rel="noopener">✨ AI Studio</a>
<a class="hlink" href="https://h-lab.tolag.shop/comment-cards" target="_blank" rel="noopener">💬 댓글 카드</a>
</div>
</div>
<div class="sub">한 URL의 여러 구간 → 이어붙여 무음컷 · 자막 · 배경템플릿 → CapCut</div>
<!-- 시그니처: 타임라인 스트립 — 원본에서 하이라이트(틸)만 골라내는 이 도구의 본질 -->
<div class="tl" aria-hidden="true">
<span class="tlc" style="width:9%"></span><span class="tlc on" style="width:7%"></span><span
class="tlc" style="width:14%"></span><span class="tlc" style="width:5%"></span><span
class="tlc on" style="width:8%"></span><span class="tlc" style="width:11%"></span><span
class="tlc" style="width:6%"></span><span class="tlc on" style="width:9%"></span><span
class="tlc" style="width:13%"></span><span class="tlc on" style="width:6%"></span><span
class="tlc" style="width:12%"></span>
<i class="tlph"></i>
</div>
</header>
<div class="tabs">
<button class="tab active" id="tab-file">📁 파일</button>
<button class="tab" id="tab-yt">▶ 유튜브 구간</button>
<button class="tab" id="tab-paste">📋 붙여넣기</button>
<button class="tab" id="tab-auto">🤖 자동</button>
</div>
<!-- 파일 모드 -->
<div id="panel-file">
<div class="helprow"><button type="button" class="helpbtn" data-help="file">? 사용법</button></div>
<div id="drop">
<div class="big">영상을 여기에 드롭하거나 클릭</div>
<div class="hint">mp4 · mov · mkv · webm</div>
<input type="file" id="file" accept="video/*,.mkv,.webm,.mov,.mp4" />
</div>
<div class="filerow" id="filerow" style="display:none;"><span class="chip" id="filechip"></span></div>
</div>
<!-- 유튜브 모드 -->
<div id="panel-yt" style="display:none;">
<div class="helprow"><button type="button" class="helpbtn" data-help="yt">? 사용법</button></div>
<div class="card" style="padding:18px;">
<div class="field"><label>유튜브 URL</label><input id="yurl" placeholder="https://www.youtube.com/watch?v=…" /></div>
<div id="ranges" style="margin-top:12px;">
<div class="row2 rng" style="align-items:end;">
<div class="field mono"><label>구간 1 시작</label><input class="rstart" placeholder="03:30" /></div>
<div class="field mono" style="display:flex;gap:6px;align-items:end;">
<div style="flex:1;"><label></label><input class="rend" placeholder="06:20" style="width:100%;" /></div>
<button type="button" class="rngdel" title="이 구간 삭제"
style="height:38px;padding:0 10px;background:var(--surf);color:var(--muted2);border:1px solid var(--border);border-radius:8px;cursor:pointer;"></button>
</div>
</div>
</div>
<button type="button" id="addrng"
style="margin-top:8px;width:100%;padding:9px;background:transparent;color:var(--accent);border:1px dashed var(--accent);border-radius:8px;cursor:pointer;font-size:13px;"> 구간 추가</button>
<div class="note" style="margin-top:8px;">여러 구간을 넣으면 순서대로 이어붙여 하나의 캡컷 드래프트로 만듭니다. 형식: 분:초(03:30) 또는 시:분:초(01:03:30)</div>
<button type="button" class="ghost" id="ytccBtn" style="margin-top:12px;">💬 구간 댓글 매칭 (h-lab · 선택사항)</button>
<div class="note" id="ytccNote" style="margin-top:4px;">구간을 언급한 댓글을 자동 선택하고, 나머지는 좋아요순으로 보여줍니다. 선택한 카드는 편집 시작 때 자동으로 들어갑니다(폴더 지정보다 우선).</div>
</div>
<div id="ytccArea"></div>
</div>
<!-- 붙여넣기(JSON) 모드 -->
<div id="panel-paste" style="display:none;">
<div class="helprow"><button type="button" class="helpbtn" data-help="paste">? 사용법</button></div>
<div class="card" style="padding:18px;">
<div class="field">
<label>편집안 JSON 붙여넣기</label>
<textarea id="pjson" spellcheck="false" rows="12"
style="width:100%;box-sizing:border-box;background:var(--surf2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:11px;font-family:var(--mono);font-size:12.5px;line-height:1.5;resize:vertical;"
placeholder='{&#10; "url": "https://www.youtube.com/watch?v=…",&#10; "title_top": "서브제목", "title_main": "메인제목", "channel": "@채널",&#10; "cuts": [&#10; {"start":"0:01.0","end":"0:03.5","bottom":"하단 자막\n두 줄","effect":"광속하강"},&#10; {"start":"2:33.5","end":"2:36.5","bottom":"다음 컷 자막","effect":"공포의통계"}&#10; ]&#10;}'></textarea>
</div>
<div class="note" style="margin-top:8px;">한 URL의 여러 컷 + 자막을 그대로 사용합니다(무음컷·받아쓰기 없음). 컷은 순서대로 이어붙고, 배치 시간은 자동 계산돼요. <b>bottom</b>=하단자막, <b>effect</b>=중앙 효과자막(하단 바로 위).</div>
</div>
</div>
<!-- 자동(Gemini) 모드 -->
<div id="panel-auto" style="display:none;">
<div class="card" style="padding:18px;">
<div class="field">
<label>방식</label>
<div style="display:flex;gap:6px;flex-wrap:wrap;">
<label class="modeopt"><input type="radio" name="amode" value="full" checked>
<span class="modetitle">🤖 AI 컷편집</span><span class="modedesc">Step1+3 · 45~60초 숏폼</span></label>
<label class="modeopt"><input type="radio" name="amode" value="whole">
<span class="modetitle">⏩ 구간 통짜</span><span class="modedesc">Step1만 · 구간 통으로</span></label>
<label class="modeopt"><input type="radio" name="amode" value="wpaste">
<span class="modetitle">📐 구간 JSON</span><span class="modedesc">구간 5개 붙여넣기 · 통짜</span></label>
<label class="modeopt"><input type="radio" name="amode" value="paste">
<span class="modetitle">📋 오팔 JSON</span><span class="modedesc">Gemini 안 씀 · 붙여넣기</span></label>
</div>
</div>
<div class="field">
<label>유튜브 URL <span id="autoUrlNote" style="color:var(--muted);font-weight:400;"></span></label>
<input id="autoUrl" placeholder="https://www.youtube.com/watch?v=…" />
</div>
<div class="field" id="apasteField" style="display:none;">
<label id="apasteLabel">오팔 JSON 붙여넣기 (여러 개를 통째로 — 사이에 구분선·타이틀 후보가 섞여 있어도 됨)</label>
<textarea id="apaste" rows="8" spellcheck="false"
style="width:100%;box-sizing:border-box;background:var(--surf2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:11px;font-family:var(--mono);font-size:12px;line-height:1.5;resize:vertical;"
placeholder='오팔 Step 3 결과(JSON 코드블록) 5개를 순서대로 전부 붙여넣으세요.&#10;블록 ②·③ 텍스트가 섞여 들어와도 자동으로 JSON만 골라냅니다.'></textarea>
</div>
<button class="run" id="autoGo" style="margin-top:8px;">분석 시작 (하이라이트 5개)</button>
<div class="note" id="autoModeHelp" style="margin-top:8px;">
Gemini가 영상에서 하이라이트 구간 5개를 골라 편집안을 만들고, h-lab에서
그 구간을 언급한 댓글을 찾아옵니다.
</div>
</div>
<details id="autoSettings" style="margin-top:12px;">
<summary>⚙ 지침·모델 수정 <span class="smhint">프롬프트 · Gemini 모델 · fps ▾</span></summary>
<div class="card" style="padding:16px;margin-top:10px;">
<div class="field"><label>Step 1 — 하이라이트 선정 프롬프트</label>
<textarea id="pStep1" rows="8" spellcheck="false"
style="width:100%;box-sizing:border-box;background:var(--surf2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:11px;font-family:var(--mono);font-size:12px;line-height:1.5;resize:vertical;"></textarea>
</div>
<div class="field"><label>Step 3 — 편집 지침서 (숏폼_편집_지침서 파일 그대로)</label>
<textarea id="pStep3" rows="10" spellcheck="false"
style="width:100%;box-sizing:border-box;background:var(--surf2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:11px;font-family:var(--mono);font-size:12px;line-height:1.5;resize:vertical;"></textarea>
</div>
<div class="field"><label>모델·샘플링 설정 (JSON)</label>
<textarea id="pConfig" rows="5" spellcheck="false"
style="width:100%;box-sizing:border-box;background:var(--surf2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:11px;font-family:var(--mono);font-size:12px;line-height:1.5;resize:vertical;"></textarea>
<div class="note" style="margin-top:2px;">
model=기본 모델 · model_step1=Step1 전용(비우면 model) ·
fps_step1/fps_step3=영상 샘플링(낮을수록 토큰 절약, 긴 영상은 0.2 권장)
</div>
</div>
<button class="ghost" id="pSave">저장</button>
<button class="ghost" id="pReset" title="Step1 프롬프트·설정만 초기화 (지침서 파일은 그대로)">기본값 복원</button>
<span class="note" id="pMsg"></span>
</div>
</details>
<div id="autoSteps"></div>
<div id="autoLog"></div>
<div id="autoReview"></div>
<button class="run" id="autoBuild" style="display:none;margin-top:14px;"></button>
<label id="autoFixedWrap" style="display:none;align-items:center;gap:7px;margin-top:10px;color:var(--muted2);font-size:12.5px;cursor:pointer;">
<input type="checkbox" id="autoCardsFixed" checked style="accent-color:var(--accent);width:15px;height:15px;"> 댓글 카드 3초 고정 (모자라도 늘리지 않고 뒤는 비움 — 부분삭제 편집용)
</label>
<div id="buildBoard"></div>
<div id="autoSummary" class="card" style="display:none;padding:18px;margin-top:12px;"></div>
</div>
<!-- 공통: 제목/출처 + 영상 옵션 -->
<div class="fields" id="commonFields">
<div id="titleGroup">
<div class="row2">
<div class="field"><label>제목 윗줄 · 주황색</label><input id="ttop" placeholder="서브제목" /></div>
<div class="field"><label>출처 (선택)</label><input id="chan" placeholder="유튜브면 채널명 자동" /></div>
</div>
<div class="field"><label>제목 아랫줄 · 흰색</label><input id="tmain" placeholder="메인제목" /></div>
<div class="note" style="margin-top:2px;">비워두면 그 텍스트는 안 들어가요. 붙여넣기 모드는 제목이 JSON에 들어갑니다.</div>
</div>
<div class="field">
<label>영상 확대 <span id="vscaleval" style="color:var(--accent);font-family:var(--mono);">144%</span></label>
<input id="vscale" type="range" value="144" min="50" max="300" step="5" style="width:100%;accent-color:var(--accent);cursor:pointer;" />
</div>
<div class="field" id="cdirField">
<label>댓글 카드 폴더 (선택)</label>
<input id="cdir" value="__CDIR__" placeholder="비우면 댓글 카드 안 넣음" />
<div class="note" style="margin-top:2px;">폴더 안 이미지를 <b>저장한 순서</b>대로 3초마다 하단에 삽입. (파일명이 1,2,3…이면 그 숫자순)</div>
</div>
<div class="field">
<label style="display:flex;align-items:center;gap:7px;cursor:pointer;">
<input type="checkbox" id="flip" style="accent-color:var(--accent);width:15px;height:15px;"> 영상 좌우반전 (미러)
</label>
</div>
<div class="field">
<label style="display:flex;align-items:center;gap:7px;cursor:pointer;">
<input type="checkbox" id="scene" checked style="accent-color:var(--accent);width:15px;height:15px;"> 장면분할 (컷 바뀌는 지점 자동 분할)
</label>
</div>
<div class="field">
<label style="display:flex;align-items:center;gap:7px;cursor:pointer;">
<input type="checkbox" id="bgwhite" checked style="accent-color:var(--accent);width:15px;height:15px;"> 배경 흰색 (위아래 띠·빈 곳을 흰색으로)
</label>
</div>
<div class="field">
<label style="display:flex;align-items:center;gap:7px;cursor:pointer;">
<input type="checkbox" id="rmsilence" style="accent-color:var(--accent);width:15px;height:15px;"> 무음 제거 (붙여넣기: 컷 안의 무음까지 잘라냄)
</label>
</div>
<div class="field">
<label style="display:flex;align-items:center;gap:7px;cursor:pointer;">
<input type="checkbox" id="asrbottom" checked style="accent-color:var(--accent);width:15px;height:15px;"> 하단 자막 자동 생성 (Whisper)
</label>
<div class="note" style="margin-top:2px;">붙여넣기: JSON의 bottom 대신, 음성을 인식해 말하는 타이밍에 맞춘 자막을 생성합니다.</div>
</div>
</div>
<button class="run" id="run">편집 시작</button>
<label style="display:flex;align-items:center;gap:7px;margin-top:10px;color:var(--muted2);font-size:12.5px;cursor:pointer;">
<input type="checkbox" id="autoopen" checked style="accent-color:var(--accent);width:15px;height:15px;"> 완료되면 CapCut 자동 실행
</label>
<div id="log"></div>
<div id="steps"></div>
<div id="result" class="card" style="padding:18px;"></div>
<!-- 레이어 수리 -->
<details id="repairBox" style="margin-top:18px;">
<summary style="cursor:pointer;color:var(--muted2);font-size:12.5px;list-style:none;">
🩹 레이어 수리 — 영상이 흰 띠·댓글 위로 삐져나올 때
</summary>
<div class="card" style="padding:16px;margin-top:10px;">
<div class="note" style="margin-top:0;line-height:1.75;">
캡컷에서 클립을 <b style="color:var(--muted2);">복붙하거나 옮기면</b> 캡컷이 그 클립의
렌더순서를 맨 위로 새로 매겨서, 확대했을 때 템플릿(흰 띠·댓글) 위로 삐져나옵니다.<br>
<b style="color:var(--muted2);">캡컷을 완전히 종료한 뒤</b> 누르세요
(실행 중이면 거부합니다 — 캡컷이 저장하면서 되돌리기 때문). 원본은
<code>draft_content.repair.bak</code>으로 백업됩니다.
</div>
<div class="field" style="margin-top:12px;">
<label>드래프트</label>
<select id="rsel" style="width:100%;background:var(--surf2);border:1px solid var(--border);
border-radius:7px;color:var(--text);font-family:var(--mono);font-size:12.5px;padding:9px 11px;outline:none;">
</select>
</div>
<button class="ghost" id="rrun" style="margin-top:10px;">레이어 수리 실행</button>
<div class="note" id="rmsg"></div>
</div>
</details>
</div>
<!-- 사용법 모달 -->
<div id="helpOverlay">
<div id="helpModal">
<button id="helpClose" title="닫기"></button>
<div id="helpBody"></div>
</div>
</div>
<script>
const $=(s)=>document.querySelector(s);
const drop=$("#drop"),fileInput=$("#file"),filerow=$("#filerow"),filechip=$("#filechip"),runBtn=$("#run");
const logEl=$("#log"),stepsEl=$("#steps"),resultEl=$("#result");
let picked=null,mode="file";
// 탭
function setMode(m){
mode=m;
$("#tab-file").classList.toggle("active",m==="file");
$("#tab-yt").classList.toggle("active",m==="yt");
$("#tab-paste").classList.toggle("active",m==="paste");
$("#tab-auto").classList.toggle("active",m==="auto");
$("#panel-file").style.display=m==="file"?"block":"none";
$("#panel-yt").style.display=m==="yt"?"block":"none";
$("#panel-paste").style.display=m==="paste"?"block":"none";
$("#panel-auto").style.display=m==="auto"?"block":"none";
// 붙여넣기·자동: 제목은 JSON에 있으니 숨김. 자동: 카드 폴더도 자동이라 숨김.
$("#commonFields").style.display="grid";
$("#titleGroup").style.display=(m==="paste"||m==="auto")?"none":"block";
$("#cdirField").style.display=m==="auto"?"none":"block";
$("#run").style.display=m==="auto"?"none":"block"; // 자동 탭은 자체 버튼 사용
}
$("#tab-file").addEventListener("click",()=>setMode("file"));
$("#tab-yt").addEventListener("click",()=>setMode("yt"));
$("#tab-paste").addEventListener("click",()=>setMode("paste"));
$("#tab-auto").addEventListener("click",()=>setMode("auto"));
// ── 사용법 모달 ──
const HELP={
file:`<h3>📁 파일 탭 사용법</h3>
<ol>
<li><b>영상 파일 드롭</b>(mp4·mov·mkv·webm) 또는 클릭해 선택</li>
<li>제목·출처는 <b>선택</b> — 비우면 캡컷에서 직접 추가</li>
<li><b>편집 시작</b> → 무음 자동 컷 + Whisper 자막 → CapCut 드래프트 생성</li>
</ol>
<ul>
<li><b>자막</b>: 말하는 타이밍에 맞춰 자동 생성(Gemini 키 있으면 글자 교정)</li>
<li><b>댓글 카드</b>: 폴더에 이미지 넣고 경로 지정 → 하단에 3초마다 1장(저장 순서, 파일명 1·2·3…이면 숫자순)</li>
<li>확대(기본 144%)·좌우반전·장면분할·배경 흰색은 옵션에서</li>
</ul>
<div class="helpdim">완료 후 CapCut에서 드래프트를 열어 확인하세요.</div>`,
yt:`<h3>▶ 유튜브 구간 탭 사용법</h3>
<ol>
<li><b>유튜브 URL</b> 입력 (실제 열리는 주소)</li>
<li><b>구간 입력</b> — 시간형식 <code>분:초</code> 또는 <code>시:분:초</code>. 숫자만 쳐도 자동 변환(4314→43:14), 끝 비우면 시작+1분30초</li>
<li><b> 구간 추가</b>로 여러 구간 → 순서대로 이어붙여 하나의 드래프트</li>
<li><b>편집 시작</b> → 다운로드·병합 → 무음 컷 + Whisper 자막 → CapCut</li>
</ol>
<ul>
<li><b>출처</b>: 비우면 유튜브 채널명 자동(@채널)</li>
<li><b>댓글 카드</b>: 폴더 경로 지정 시 하단에 3초마다 1장 자동 삽입</li>
</ul>
<div class="helpdim">긴 구간일수록 다운로드·자막(1분당 ≈30초) 시간이 늘어납니다.</div>`,
paste:`<h3>📋 붙여넣기 탭 사용법</h3>
<ol>
<li>LLM(AI Studio 등)에게 아래 형식의 <b>JSON 하나</b>를 받아 붙여넣기</li>
<li><b>편집 시작</b> → 컷 정밀 다운로드·병합 → 자막 → CapCut</li>
</ol>
<pre>{
"url": "https://www.youtube.com/watch?v=실제ID",
"title_top": "서브제목", "title_main": "메인제목",
"channel": "@채널",
"cuts": [
{"start":"0:01.0","end":"0:03.5",
"bottom":"자막 윗줄\\n아랫줄","effect":"(효과자막)"}
]
}</pre>
<ul>
<li><b>url은 실제 영상 주소</b> — LLM이 지어낸 ID는 실패</li>
<li>배치 시간은 넣지 마세요 — 컷 순서대로 자동 계산</li>
<li><b>하단 자막 자동 생성(Whisper) 켜짐</b>: bottom 무시, 실제 말 타이밍 자막 생성(추천)</li>
<li>끄면: bottom 사용, \\n 은 시간 반씩 나눠 윗줄→아랫줄</li>
<li><b>effect</b>: 중앙 녹색 효과자막 · <b>댓글 카드</b>: 폴더 지정 시 3초마다 1장</li>
<li><b>무음 제거</b> 켜면 컷 안의 무음까지 컷(자막 자동 보정)</li>
</ul>
<div class="helpdim">헤더의 ✨ AI Studio에서 JSON 생성, 💬 댓글 카드에서 카드 저장.</div>`};
const overlay=$("#helpOverlay");
document.querySelectorAll(".helpbtn").forEach(b=>b.addEventListener("click",()=>{
$("#helpBody").innerHTML=HELP[b.dataset.help]||"";
overlay.style.display="flex";
}));
$("#helpClose").addEventListener("click",()=>overlay.style.display="none");
overlay.addEventListener("click",e=>{if(e.target===overlay)overlay.style.display="none";});
document.addEventListener("keydown",e=>{if(e.key==="Escape")overlay.style.display="none";});
setMode("paste"); // 붙여넣기 탭 기본 활성화
// 시간 입력 자동 변환(4314→43:14, 0302→03:02) + 시작 입력시 끝=시작+1:30
function fmtTime(v){
const d=(v||"").replace(/\D/g,"");
if(!d) return "";
const sec=d.slice(-2).padStart(2,"0");
const min=d.slice(-4,-2);
const hour=d.slice(0,-4);
if(hour) return `${hour}:${(min||"0").padStart(2,"0")}:${sec}`;
if(min) return `${min}:${sec}`;
return `0:${sec}`;
}
function toSec(t){
const p=(t||"").split(":").map(Number);
if(p.some(isNaN)||!p.length) return NaN;
return p.length===3? p[0]*3600+p[1]*60+p[2] : p.length===2? p[0]*60+p[1] : p[0];
}
function fmtSec(s){
s=Math.max(0,Math.round(s));
const h=Math.floor(s/3600), m=Math.floor(s%3600/60), ss=String(s%60).padStart(2,"0");
return h? `${h}:${String(m).padStart(2,"0")}:${ss}` : `${m}:${ss}`;
}
// 여러 구간: 자동 시간변환(focusout 위임) + 시작 입력시 끝=시작+1:30
const rangesEl=$("#ranges");
if(rangesEl){
rangesEl.addEventListener("focusout",ev=>{
const el=ev.target;
if(el.classList.contains("rstart")){
if(!el.value.trim())return;
el.value=fmtTime(el.value);
const row=el.closest(".rng"), end=row.querySelector(".rend");
const s=toSec(el.value);
if(end&&!end.value.trim()&&!isNaN(s)) end.value=fmtSec(s+90);
}else if(el.classList.contains("rend")){
if(el.value.trim()) el.value=fmtTime(el.value);
}
});
// 구간 삭제(위임) — 최소 1개는 남김
rangesEl.addEventListener("click",ev=>{
if(!ev.target.classList.contains("rngdel"))return;
if(rangesEl.querySelectorAll(".rng").length<=1)return;
ev.target.closest(".rng").remove();
renumberRanges();
});
}
function renumberRanges(){
rangesEl.querySelectorAll(".rng").forEach((row,i)=>{
const lb=row.querySelector(".rstart")?.closest(".field")?.querySelector("label");
if(lb) lb.textContent=`구간 ${i+1} 시작`;
});
}
const addRngBtn=$("#addrng");
if(addRngBtn){
addRngBtn.addEventListener("click",()=>{
const row=rangesEl.querySelector(".rng").cloneNode(true);
row.querySelectorAll("input").forEach(i=>i.value="");
rangesEl.appendChild(row);
renumberRanges();
row.querySelector(".rstart")?.focus();
});
}
// 영상 확대 슬라이더 → % 표시
const vs=$("#vscale"); if(vs){ vs.addEventListener("input",()=>{$("#vscaleval").textContent=vs.value+"%";}); }
drop.addEventListener("click",()=>fileInput.click());
fileInput.addEventListener("change",()=>{if(fileInput.files[0])setFile(fileInput.files[0]);});
["dragenter","dragover"].forEach(e=>drop.addEventListener(e,ev=>{ev.preventDefault();drop.classList.add("drag");}));
["dragleave","drop"].forEach(e=>drop.addEventListener(e,ev=>{ev.preventDefault();drop.classList.remove("drag");}));
drop.addEventListener("drop",ev=>{const f=ev.dataTransfer.files[0];if(f)setFile(f);});
function fmtSize(b){if(b>1e9)return(b/1e9).toFixed(1)+" GB";if(b>1e6)return(b/1e6).toFixed(1)+" MB";return(b/1e3).toFixed(0)+" KB";}
function setFile(f){picked=f;filechip.innerHTML=`<span class="k">●</span> ${f.name} <span class="k tnum">· ${fmtSize(f.size)}</span>`;filerow.style.display="block";}
function addLog(m){logEl.style.display="block";const d=document.createElement("div");d.className="line";d.textContent=m;logEl.appendChild(d);}
function titleFields(fd){fd.append("title_top",$("#ttop").value);fd.append("title_main",$("#tmain").value);fd.append("channel",$("#chan").value);fd.append("video_scale",$("#vscale").value||"144");fd.append("flip",$("#flip").checked?"1":"0");fd.append("scene",$("#scene").checked?"1":"0");fd.append("comments_dir",$("#cdir").value.trim());fd.append("bg_white",$("#bgwhite").checked?"1":"0");}
runBtn.addEventListener("click",async()=>{
window._runStart=Date.now(); // 시작~끝 총 시간 측정
logEl.innerHTML="";resultEl.style.display="none";resultEl.innerHTML="";
stepsEl.style.display="none";stepsEl.innerHTML="";
let res;
if(mode==="file"){
if(!picked){addLog("파일을 먼저 선택하세요.");return;}
runBtn.disabled=true;runBtn.textContent="업로드 중…";
const fd=new FormData();fd.append("file",picked);titleFields(fd);
try{res=await(await fetch("/upload",{method:"POST",body:fd})).json();}
catch(e){return fail("업로드 실패: "+e);}
}else if(mode==="yt"){
const url=$("#yurl").value.trim();
if(!url){addLog("유튜브 URL을 입력하세요.");return;}
// 모든 구간 수집(빈 행 제외). 끝 비면 시작+1:30
const ranges=[];
rangesEl.querySelectorAll(".rng").forEach(row=>{
const si=row.querySelector(".rstart"), ei=row.querySelector(".rend");
let st=fmtTime(si.value);
if(!st) return;
let en=ei.value.trim()?fmtTime(ei.value):fmtSec(toSec(st)+90);
si.value=st; ei.value=en;
ranges.push([st,en]);
});
if(!ranges.length){addLog("구간을 하나 이상 입력하세요.");return;}
runBtn.disabled=true;runBtn.textContent="처리 중…";
const fd=new FormData();
fd.append("url",url);fd.append("ranges",JSON.stringify(ranges));titleFields(fd);
const yf=document.querySelector("#ytccFixed");
if(yf) fd.append("cards_fixed",yf.checked?"1":"0");
// 댓글 매칭에서 카드를 골랐으면 캡처해서 함께 전송(폴더 지정보다 우선)
if(window.ytCC&&window.ytCC.active()){
runBtn.textContent="댓글 카드 캡처 중…";
const blobs=await window.ytCC.capture();
blobs.forEach((b,i)=>fd.append("cards",b,String(i+1).padStart(3,"0")+".png"));
runBtn.textContent="처리 중…";
}
try{res=await(await fetch("/youtube",{method:"POST",body:fd})).json();}
catch(e){return fail("요청 실패: "+e);}
if(res&&res.error){return fail(res.error);}
}
if(mode==="paste"){
const txt=$("#pjson").value.trim();
if(!txt){addLog("편집안 JSON을 붙여넣으세요.");return;}
runBtn.disabled=true;runBtn.textContent="처리 중…";
const fd=new FormData();fd.append("data",txt);
fd.append("video_scale",$("#vscale").value||"144");
fd.append("flip",$("#flip").checked?"1":"0");
fd.append("scene",$("#scene").checked?"1":"0");
fd.append("comments_dir",$("#cdir").value.trim());
fd.append("bg_white",$("#bgwhite").checked?"1":"0");
fd.append("remove_silence",$("#rmsilence").checked?"1":"0");
fd.append("asr_bottom",$("#asrbottom").checked?"1":"0");
try{res=await(await fetch("/paste",{method:"POST",body:fd})).json();}
catch(e){return fail("요청 실패: "+e);}
if(res&&res.error){return fail(res.error);}
}
runBtn.textContent="처리 중…";
runStream(res.job_id);
});
function runStream(id){
const es=new EventSource(`/stream/${id}`);
es.onmessage=(m)=>{
const ev=JSON.parse(m.data);
if(ev.type==="manifest")renderSteps(ev.steps);
else if(ev.type==="log")addLog(ev.msg);
else if(ev.type==="step")updateStep(ev);
else if(ev.type==="result"){es.close();renderResult(ev);finish();}
else if(ev.type==="error"){es.close();stepError(ev.message);finish();}
};
es.onerror=()=>{es.close();if(!resultEl.innerHTML)stepError("연결이 끊겼습니다.");finish();};
}
function finish(){runBtn.disabled=false;runBtn.textContent="다시 실행";}
function fail(m){stepError(m);finish();}
function renderSteps(steps){
stepsEl.style.display="block";
stepsEl.innerHTML=steps.map(s=>`<div class="step" id="st-${s.id}"><span class="sdot"></span>
<div class="slabel"><div class="t">${s.label}</div><div class="sdetail" hidden></div></div>
<div class="selapsed"></div></div>`).join("");
}
function updateStep(ev){
const el=$(`#st-${ev.id}`);if(!el)return;
if(ev.status==="start"){document.querySelectorAll(".step.active").forEach(s=>s.classList.remove("active"));el.classList.add("active");}
else if(ev.status==="done"){el.classList.remove("active");el.classList.add("done");
if(ev.detail){const d=el.querySelector(".sdetail");d.hidden=false;d.textContent=ev.detail;}
if(ev.elapsed!=null)el.querySelector(".selapsed").textContent=ev.elapsed.toFixed(1)+"s";}
}
function stepError(msg){
const a=document.querySelector(".step.active")||document.querySelector(".step:not(.done)");
if(a){a.classList.remove("active");a.classList.add("err");const d=a.querySelector(".sdetail");d.hidden=false;d.textContent=msg;}
else{stepsEl.style.display="block";stepsEl.innerHTML=`<div class="step err"><span class="sdot"></span><div class="slabel"><div class="t">오류</div><div class="sdetail">${msg}</div></div></div>`;}
}
function renderResult(ev){
const s=ev.stats;
const total=window._runStart?((Date.now()-window._runStart)/1000).toFixed(1):s.elapsed;
resultEl.style.display="block";
resultEl.innerHTML=`
<div style="font-family:var(--mono);font-size:13.5px;color:var(--accent);margin-bottom:14px;">⏱ 총 ${total}초 소요</div>
<div class="grid3">
<div class="stat"><div class="v tnum">${s.duration}s</div><div class="l">원본</div></div>
<div class="stat"><div class="v green tnum">${s.cut}s</div><div class="l">무음 컷</div></div>
<div class="stat"><div class="v tnum">${s.captions}</div><div class="l">자막</div></div>
</div>
<hr class="sep" />
<div class="reslabel">드래프트 · ${s.elapsed}s</div>
<div><span class="chip" title="${ev.draft_path}">${ev.draft_name}</span></div>
<button class="capcut" id="capcut">▶ CapCut 실행</button>
<button class="ghost" id="copyp">드래프트 경로 복사</button>
<div class="opennote">CapCut 실행 후 프로젝트 목록에서 <b>${ev.draft_name}</b> 열기<br>
하단 검은 영역에 댓글 캡쳐를 직접 넣으세요</div>`;
const openCapcut=async()=>{
$("#capcut").textContent="실행 중…";
try{const r=await(await fetch("/open-capcut",{method:"POST"})).json();
$("#capcut").textContent=r.ok?"✓ CapCut 열림":"실행 실패 — 직접 열어주세요";}
catch(e){$("#capcut").textContent="실행 실패 — 직접 열어주세요";}
};
$("#capcut").addEventListener("click",openCapcut);
// 완료 시 자동 실행(체크박스 켜짐일 때)
if($("#autoopen")&&$("#autoopen").checked) setTimeout(openCapcut,500);
$("#copyp").addEventListener("click",()=>{navigator.clipboard.writeText(ev.draft_path);
$("#copyp").textContent="복사됨";setTimeout(()=>$("#copyp").textContent="드래프트 경로 복사",1200);});
}
// ── 레이어 수리 ──
async function loadDrafts(){
const sel=$("#rsel"); if(!sel) return;
try{
const r=await(await fetch("/drafts")).json();
// 캡컷에서 이름을 바꾸면 폴더명(=수리 키)과 화면 이름이 갈린다 → 화면 이름으로 찾게 한다
const esc=s=>String(s).replace(/[&<>"]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
sel.innerHTML=(r.drafts||[]).map(d=>{
const t=d.title||d.name;
// value 는 폴더 경로 — 드래프트가 기본 경로 밖(D: 등)에도 있어 폴더명만으론 특정 불가
return `<option value="${esc(d.path||d.name)}">${d.broken?"⚠ ":"✓ "}${esc(t)}`
+`${t!==d.name?` [폴더: ${esc(d.name)}]`:""}`
+`${d.broken?` (꼬임 ${d.bad}개)`:""}</option>`;
}).join("")||`<option value="">드래프트 없음</option>`;
}catch(e){ sel.innerHTML=`<option value="">목록 불러오기 실패</option>`; }
}
if($("#repairBox")){
$("#repairBox").addEventListener("toggle",()=>{ if($("#repairBox").open) loadDrafts(); });
$("#rrun").addEventListener("click",async()=>{
const name=$("#rsel").value; if(!name) return;
$("#rrun").disabled=true; $("#rrun").textContent="수리 중…"; $("#rmsg").textContent="";
try{
const fd=new FormData(); fd.append("draft",name);
const r=await(await fetch("/repair",{method:"POST",body:fd})).json();
$("#rmsg").textContent=r.ok
? (r.fixed?`${r.fixed}개 세그먼트 원위치 (${Object.entries(r.detail).map(([k,v])=>k+" "+v).join(", ")}) — 파일 ${r.files}개 수정, 캡컷에서 다시 열어보세요`
:"✓ 꼬인 곳 없음 (잠금만 다시 채움)")
: `${r.error}`;
$("#rmsg").style.color = r.ok ? "" : (r.capcut_running ? "var(--accent)" : "var(--danger)");
$("#rmsg").style.whiteSpace = "pre-line";
loadDrafts();
}catch(e){ $("#rmsg").textContent="✗ 요청 실패"; }
$("#rrun").disabled=false; $("#rrun").textContent="레이어 수리 실행";
});
}
</script>
<script src="/static/modern-screenshot.js?v=__V__"></script>
<script src="/static/auto.js?v=__V__"></script>
</body>
</html>