검은 카드가 검은 배경에 묻혀 윤곽이 안 보이던 문제 — 캡처 공용 함수 captureCard() 가 흰색 배경 체크 상태를 보고 캡처 순간에만 .light(흰 바탕 + 짙은 글씨)를 붙였다 뗀다. 세 탭 공용, 검토 화면 표시는 어두운 테마 유지. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
900 lines
58 KiB
HTML
900 lines
58 KiB
HTML
<!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;}
|
||
/* 본문 최대 5줄 말줄임(…) — 카드는 캡처돼 드래프트 하단(영상 아래~출처 사이 약 640px)에
|
||
들어가는데, 긴 댓글은 세로가 무한정 자라 출처(@채널)를 덮는다. 5줄이면 표시 높이가
|
||
그 한계 안이고, 검토 화면 = 캡처 결과(WYSIWYG)라 여기서 자르는 게 맞다. */
|
||
.cc-text{font-size:14px;color:#fff;margin-top:5.6px;white-space:pre-wrap;word-break:break-word;line-height:1.5;
|
||
display:-webkit-box;-webkit-line-clamp:5;-webkit-box-orient:vertical;overflow:hidden;}
|
||
.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);}
|
||
/* 흰 카드 테마 — 배경을 검정으로 만들 때(흰색 배경 체크 해제) 캡처 직전에만 붙는다.
|
||
검은 카드는 검은 배경에 묻혀 윤곽이 안 보이므로 캡처용으로 반전(흰 바탕 + 짙은 글씨).
|
||
검토 화면 표시는 어두운 테마 그대로 — captureCard() 가 붙였다 뗀다. */
|
||
.comment-card.light{background:#fff;border-color:#ddd;}
|
||
.comment-card.light .cc-author{color:#222;}
|
||
.comment-card.light .cc-text{color:#111;}
|
||
.comment-card.light .cc-time,.comment-card.light .cc-stats{color:#888;}
|
||
/* 검토 화면 — 본문 600px 유지, 검토 영역만 와이드 브레이크아웃(댓글 카드 3~4열) */
|
||
#autoPickReview,#autoReview,#autoSummary,#ytReview,#ytSummary,#pasteReview,#pasteSummary{
|
||
--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;}
|
||
/* ── 1차 검토(하이라이트 카드) — 댓글 없이 제목만 고르는 화면. .hlbox 를 그대로 쓰되
|
||
탭 전환 없이 세로로 쌓아 보여준다(하이라이트가 5개뿐이라 전부 보여도 부담 없음). ── */
|
||
.pickcard{margin-top:10px;display:block;}
|
||
.pickcard:first-child{margin-top:4px;}
|
||
.pickhead{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:6px;}
|
||
.pickx{width:auto;margin:0 0 0 auto;padding:6px 12px;flex:none;}
|
||
.pickcard.dropped-pick{opacity:.5;border-style:dashed;}
|
||
.pickcard.dropped-pick .pickx{color:var(--accent);}
|
||
.pickcard.errbox{border-color:var(--danger);}
|
||
/* ── 영상 편집안(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;}
|
||
/* ── 컷별 카드 아코디언 — 한 번에 한 컷만 펼쳐 후보 카드가 한꺼번에 쌓이지 않게 한다 ── */
|
||
.cutgrp{margin-top:10px;border:1px solid var(--border);border-radius:9px;
|
||
background:var(--surf2);overflow:hidden;}
|
||
.cutgrp-head{width:100%;box-sizing:border-box;display:flex;align-items:center;gap:8px;
|
||
text-align:left;cursor:pointer;background:transparent;border:0;color:var(--muted2);
|
||
padding:11px 13px;min-height:44px;font-size:12.5px;font-weight:600;transition:background .15s,color .15s;}
|
||
.cutgrp-head:hover{color:var(--text);background:var(--surf);}
|
||
.cutgrp.open>.cutgrp-head{color:var(--text);border-bottom:1px solid var(--border);}
|
||
.cutgrp-head .chev{flex:none;color:var(--muted);font-size:10px;transition:transform .18s ease;}
|
||
.cutgrp.open .chev{transform:rotate(90deg);}
|
||
.cutgrp-head .cgstat{margin-left:auto;flex:none;font-family:var(--mono);font-size:11.5px;
|
||
color:var(--muted);font-weight:400;}
|
||
.cutgrp.open .cgstat{color:var(--accent);}
|
||
.cutgrp-body{display:none;padding:0 13px 12px;}
|
||
.cutgrp.open .cutgrp-body{display:block;}
|
||
/* 선택한 댓글 요약 바 */
|
||
.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>
|
||
</div>
|
||
<!-- 제목·출처 — 검토 영역 위. 분석에 시간이 걸리므로 분석 후 오타를 발견해도
|
||
재분석 없이 여기서 고치면 생성(드래프트 만들기)에 그대로 반영된다. -->
|
||
<div class="card" style="padding:18px;margin-top:10px;">
|
||
<div class="row2">
|
||
<div class="field"><label>제목 윗줄 · 주황색</label><input id="yttop" placeholder="서브제목" /></div>
|
||
<div class="field"><label>출처 (선택)</label><input id="ytchan" placeholder="유튜브면 채널명 자동" /></div>
|
||
</div>
|
||
<div class="field"><label>제목 아랫줄 · 흰색</label><input id="ytmain" placeholder="메인제목" /></div>
|
||
<div class="note" style="margin-top:2px;">비워두면 그 텍스트는 안 들어가요. 분석이 끝난 뒤에도 고칠 수 있고, 드래프트를 만들 때 여기 값이 반영됩니다.</div>
|
||
</div>
|
||
<div id="ytSteps"></div>
|
||
<div id="ytLog"></div>
|
||
<div id="ytReview"></div>
|
||
<button class="run" id="ytBuild" style="display:none;margin-top:14px;">선택한 카드로 드래프트 만들기</button>
|
||
<div id="ytBoard" style="display:none;margin-top:10px;"></div>
|
||
<div id="ytSummary" class="card" style="display:none;padding:18px;margin-top:12px;"></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='{ "url": "https://www.youtube.com/watch?v=…", "title_top": "서브제목", "title_main": "메인제목", "channel": "@채널", "cuts": [ {"start":"0:01.0","end":"0:03.5","bottom":"하단 자막\n두 줄","effect":"광속하강"}, {"start":"2:33.5","end":"2:36.5","bottom":"다음 컷 자막","effect":"공포의통계"} ] }'></textarea>
|
||
</div>
|
||
<div class="note" style="margin-top:8px;">한 URL의 여러 컷 + 자막을 그대로 사용합니다. 컷은 순서대로 이어붙고, 배치 시간은 자동 계산돼요. <b>bottom</b>=하단자막, <b>effect</b>=중앙 효과자막(하단 바로 위). 무음 제거·받아쓰기(Whisper)는 댓글 매칭을 위해 항상 적용됩니다.</div>
|
||
<button class="run" id="pasteGo" style="margin-top:8px;">댓글 매칭 시작</button>
|
||
</div>
|
||
|
||
<div id="pasteSteps"></div>
|
||
<div id="pasteLog"></div>
|
||
<div id="pasteReview"></div>
|
||
<button class="run" id="pasteBuild" style="display:none;margin-top:14px;">선택한 카드로 드래프트 만들기</button>
|
||
<div id="pasteBoard" style="display:none;margin-top:10px;"></div>
|
||
<div id="pasteSummary" class="card" style="display:none;padding:18px;margin-top:12px;"></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개를 순서대로 전부 붙여넣으세요. 블록 ②·③ 텍스트가 섞여 들어와도 자동으로 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>
|
||
<!-- 1차 검토 — 하이라이트 카드(제목 선택 · ✕ 제외 · 편집안 접기). 댓글 영역 없음. -->
|
||
<div id="autoPickReview"></div>
|
||
<button class="run" id="autoPrepGo" style="display:none;margin-top:14px;"></button>
|
||
<div id="autoPrepSteps"></div>
|
||
<div id="autoPrepLog"></div>
|
||
<!-- 2차 검토 — /auto/prepare 가 준비한 ID만 컷별 댓글 선택(renderCutPanel, 기존과 동일). -->
|
||
<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" id="rmsilenceField">
|
||
<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에 있으니 숨김. 구간: 검토 영역 위 전용 입력칸(#yttop 등)을 쓰므로 숨김.
|
||
// 자동: 카드 폴더도 자동이라 숨김.
|
||
$("#commonFields").style.display="grid";
|
||
$("#titleGroup").style.display=(m==="paste"||m==="auto"||m==="yt")?"none":"block";
|
||
$("#cdirField").style.display=m==="auto"?"none":"block";
|
||
$("#run").style.display=(m==="auto"||m==="paste")?"none":"block"; // 자동·붙여넣기 탭은 자체 버튼 사용
|
||
// 붙여넣기·자동 탭은 댓글 매칭을 위해 무음 제거·받아쓰기가 항상 켜진다(/paste·/auto prepare
|
||
// 가 remove_silence=True 로 고정) — 끌 수 있는 것처럼 보이면 안 되므로 체크박스 자체를 숨긴다.
|
||
$("#rmsilenceField").style.display=(m==="paste"||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 자막 → h-lab 댓글 수집·구간별 추천</li>
|
||
<li>검토 화면에서 구간별 추천 댓글 카드를 확인·선택하고 <b>선택한 카드로 드래프트 만들기</b></li>
|
||
</ol>
|
||
<ul>
|
||
<li><b>제목·출처</b>: 검토 영역 위 입력칸 — 분석이 끝난 뒤 오타를 고쳐도 재분석 없이 드래프트에 반영됩니다. 출처를 비워두면 유튜브 채널명 자동(@채널)</li>
|
||
<li><b>댓글 카드</b>: 검토에서 하나도 안 고르면 예전처럼 <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> → 컷 정밀 다운로드·병합 → 무음 제거 → 받아쓰기 → h-lab 댓글 매칭</li>
|
||
<li>컷마다 추천된 댓글 카드를 확인·선택하고 <b>선택한 카드로 드래프트 만들기</b></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>됩니다(끌 수 없음)</li>
|
||
<li><b>하단 자막 자동 생성(Whisper) 켜짐</b>: bottom 무시, 실제 말 타이밍 자막 생성(추천)</li>
|
||
<li>끄면(드래프트 만들기 단계): bottom 사용, \\n 은 시간 반씩 나눠 윗줄→아랫줄</li>
|
||
<li><b>effect</b>: 중앙 녹색 효과자막</li>
|
||
<li><b>댓글 카드</b>: 컷별 추천에서 카드를 하나도 안 고르면 예전처럼 <b>댓글 카드 폴더</b> 지정으로 동작(3초마다 1장)</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="";
|
||
if(mode==="yt"){
|
||
// ▶ 유튜브 구간 탭: 이제 "편집 시작"이 분석(analyze→SSE→검토→생성)을 돈다 —
|
||
// 📋 붙여넣기 탭과 같은 흐름(auto.js 의 ytAnalyze). 진행·결과는 #ytSteps/#ytReview 등
|
||
// 패널 전용 영역에 그려지므로 여기서는 넘기고 끝(#log/#steps/#result 는 안 씀).
|
||
if(window.ytAnalyze) window.ytAnalyze();
|
||
return;
|
||
}
|
||
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);}
|
||
}
|
||
// 📋 붙여넣기 탭은 자체 버튼(#pasteGo → #pasteBuild, auto.js)으로 도므로 여기 안 옴.
|
||
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=>({"&":"&","<":"<",">":">",'"':"""}[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>
|