From f38776d71fc5baad879ce1c01325b7f074a23355 Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Fri, 7 Aug 2026 12:13:19 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B5=AC=EA=B0=84=20JSON=EC=97=90=20ti?= =?UTF-8?q?tle=5Ftop/title=5Fmain=20=EC=84=A0=ED=83=9D=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=E2=80=94=20=EA=B2=80=ED=86=A0=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EC=A0=9C=EB=AA=A9=20=EC=9E=90=EB=8F=99=20=EC=B1=84=EC=9B=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- capcut_agent/plan.py | 8 ++++++-- server/app.py | 7 +++++-- server/static/auto.js | 11 ++++++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/capcut_agent/plan.py b/capcut_agent/plan.py index 02b8e0d..8f49288 100644 --- a/capcut_agent/plan.py +++ b/capcut_agent/plan.py @@ -93,10 +93,12 @@ def _extract_json_str(text: str) -> str: def parse_candidates(text: str, *, src: str = "Step 1 응답") -> List[Dict]: - """`{"candidates":[{id,start_time,end_time,reason}, …]}` → [{id,start,end,reason}] (초). + """`{"candidates":[{id,start_time,end_time,reason}, …]}` → [{id,start,end,reason,title_top,title_main}] (초). Gemini Step1 응답과 사용자가 직접 붙여넣는 구간 JSON이 **같은 형식**이라 둘이 공유한다. 코드펜스(```)·앞뒤 잡텍스트는 `_extract_json_str`이 걷어낸다. + `title_top`/`title_main`은 선택 — 구간 JSON에 있으면 검토 화면 제목 입력칸에 + 미리 채워진다(Step1 응답에는 없으므로 빈 문자열). """ try: data = json.loads(_extract_json_str(text), strict=False) @@ -116,7 +118,9 @@ def parse_candidates(text: str, *, src: str = "Step 1 응답") -> List[Dict]: continue if e > s: out.append({"id": int(c.get("id") or i), "start": s, "end": e, - "reason": str(c.get("reason") or "").strip()}) + "reason": str(c.get("reason") or "").strip(), + "title_top": str(c.get("title_top") or "").strip(), + "title_main": str(c.get("title_main") or "").strip()}) if not out: raise RuntimeError(f"{src}: 유효한 구간이 하나도 없습니다.") return out diff --git a/server/app.py b/server/app.py index d988753..74bf46f 100644 --- a/server/app.py +++ b/server/app.py @@ -526,13 +526,16 @@ async def auto_stream(aid: str) -> StreamingResponse: def _whole_hl(c: dict) -> dict: """구간 통짜 하이라이트 — 컷 편집 없이 구간 전체가 컷 1개. - 제목은 비워 두고 `editable_title` 로 UI에서 직접 입력받는다. + 제목은 `editable_title` 로 UI에서 직접 입력받되, 구간 JSON에 + title_top/title_main 이 있으면 그 값을 입력칸에 미리 채운다. whole(Gemini Step1) 과 wpaste(구간 JSON 붙여넣기) 가 공유.""" total = c["end"] - c["start"] return { "id": c["id"], "start": c["start"], "end": c["end"], "reason": c["reason"], - "paste": {"url": url, "title_top": "", "title_main": "", + "paste": {"url": url, + "title_top": c.get("title_top") or "", + "title_main": c.get("title_main") or "", "channel": "", "cuts": [{"start": c["start"], "end": c["end"], "bottom": "", "effect": ""}]}, diff --git a/server/static/auto.js b/server/static/auto.js index cbda7f3..e00b5b3 100644 --- a/server/static/auto.js +++ b/server/static/auto.js @@ -532,15 +532,15 @@ function curMode(){ const MODE_HELP={ full:"Gemini가 영상에서 하이라이트 구간 5개를 골라 편집안을 만들고, h-lab에서 그 구간을 언급한 댓글을 찾아옵니다. 무음 제거·받아쓰기(Whisper)는 댓글 매칭을 위해 항상 적용됩니다(끌 수 없음).", whole:"Gemini가 구간 5개만 고르고(Step1), 각 구간을 컷 편집 없이 통짜로 만듭니다. 무음 제거·받아쓰기(Whisper)는 댓글 매칭을 위해 항상 적용됩니다(끌 수 없음).", - wpaste:"Gemini를 쓰지 않습니다. 구간 JSON(candidates 5개)을 붙여넣으면 그 구간을 그대로 통짜로 만듭니다. 제목(윗줄·아랫줄)은 댓글 선택 화면에서 ID별로 직접 씁니다. 무음 제거·받아쓰기(Whisper)는 댓글 매칭을 위해 항상 적용됩니다(끌 수 없음).", + wpaste:"Gemini를 쓰지 않습니다. 구간 JSON(candidates 5개)을 붙여넣으면 그 구간을 그대로 통짜로 만듭니다. JSON에 title_top·title_main이 있으면 제목(윗줄·아랫줄)이 자동으로 채워지고, 검토 화면에서 ID별로 고칠 수 있습니다. 무음 제거·받아쓰기(Whisper)는 댓글 매칭을 위해 항상 적용됩니다(끌 수 없음).", paste:"Gemini를 쓰지 않습니다. 오팔에서 받은 JSON 5개를 통째로 붙여넣으면 댓글 선택 화면으로 갑니다. URL을 비우면 JSON 안의 url을 씁니다. 무음 제거·받아쓰기(Whisper)는 댓글 매칭을 위해 항상 적용됩니다(끌 수 없음).", }; const PASTE_UI={ paste:{label:"오팔 JSON 붙여넣기 (여러 개를 통째로 — 사이에 구분선·타이틀 후보가 섞여 있어도 됨)", ph:'오팔 Step 3 결과(JSON 코드블록) 5개를 순서대로 전부 붙여넣으세요.\n블록 ②·③ 텍스트가 섞여 들어와도 자동으로 JSON만 골라냅니다.', note:"(선택 — 비우면 JSON의 url 사용)"}, - wpaste:{label:"구간 JSON 붙여넣기 (candidates 배열 — 구간 5개)", - ph:'{\n "candidates": [\n {"id": 1, "start_time": "14:28", "end_time": "16:15", "reason": "구간 선정 이유"},\n … (총 5개)\n ]\n}', + wpaste:{label:"구간 JSON 붙여넣기 (candidates 배열 — 구간 5개 · title_top/title_main 선택)", + ph:'{\n "candidates": [\n {"id": 1, "start_time": "14:28", "end_time": "16:15", "reason": "구간 선정 이유",\n "title_top": "상단 제목 (선택)", "title_main": "메인 제목 (선택)"},\n … (총 5개)\n ]\n}', note:"(필수 — 구간 JSON에는 URL이 없습니다)"}, }; function applyMode(){ @@ -654,6 +654,11 @@ function onResult(ev){ (t.kind?" — "+esc(t.kind):"")+"").join("")+""; } card.innerHTML=html; + if(hl.editable_title){ // 구간 JSON의 title_top/title_main 미리 채움 (.value라 따옴표 이스케이프 불필요) + const ti=card.querySelector("#hltop-"+hl.id),mi=card.querySelector("#hlmain-"+hl.id); + if(ti) ti.value=hl.paste.title_top||""; + if(mi) mi.value=hl.paste.title_main||""; + } card.dataset.titles=JSON.stringify(opts); card.appendChild(cutsSection(hl)); // 영상 편집안(컷 목록·JSON) — 기본 접힘 R.appendChild(card);