feat: 구간 JSON에 title_top/title_main 선택 필드 — 검토 화면 제목 자동 채움

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hehihoho3@gmail.com 2026-08-07 12:13:19 +09:00
parent b49946a4cd
commit f38776d71f
3 changed files with 19 additions and 7 deletions

View File

@ -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

View File

@ -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": ""}]},

View File

@ -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):"")+"</option>").join("")+"</select></div>";
}
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);