capcut-agent/server/static/index.html
hehihoho3@gmail.com b5586ecb9c feat: 배경 검정이면 댓글 카드를 흰색 테마로 캡처
검은 카드가 검은 배경에 묻혀 윤곽이 안 보이던 문제 — 캡처 공용 함수
captureCard() 가 흰색 배경 체크 상태를 보고 캡처 순간에만 .light(흰 바탕
+ 짙은 글씨)를 붙였다 뗀다. 세 탭 공용, 검토 화면 표시는 어두운 테마 유지.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 09:23:15 +09:00

900 lines
58 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;}
/* 본문 최대 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='{&#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>=중앙 효과자막(하단 바로 위). 무음 제거·받아쓰기(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개를 순서대로 전부 붙여넣으세요.&#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>
<!-- 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=>({"&":"&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>