From 6ab3d998e95a266f151e4ecc84f711dfd1115519 Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Wed, 5 Aug 2026 13:26:55 +0900 Subject: [PATCH] =?UTF-8?q?fix(critical):=20=EC=9C=A0=ED=8A=9C=EB=B8=8C=20?= =?UTF-8?q?=EA=B5=AC=EA=B0=84=20=ED=83=AD=20=EC=8B=9C=EA=B0=84=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=20=EB=B6=88=EC=9D=BC=EC=B9=98=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=EB=A9=B4=20=EB=B6=88=EB=8A=A5=EC=9D=B4=EB=8D=98=20=EA=B2=83=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ytRanges()가 구간을 초 숫자로 /yt/analyze에 보내는데 다운로드 경로의 cut_youtube()→valid_time()은 MM:SS/HH:MM:SS만 통과시켜, 모든 분석이 다운로드 단계에서 "시간 형식 오류"로 죽고 있었다. 프런트는 입력칸에 이미 들어있는 fmtTime 포맷 문자열을 그대로 보내도록 되돌리고, 서버 (_parse_ranges)도 parse_time()으로 초 환산 후 valid_time이 통과하는 형식으로 재포맷해 프런트가 무엇을 보내든 다운로드 경로까지 안전하게 닿도록 방어를 추가했다. 스크래치패드 스크립트로 정상 케이스·구 버그 케이스·1시간 이상 구간까지 valid_time 통과를 확인했다. Co-Authored-By: Claude Opus 5 (1M context) --- server/app.py | 32 +++++++++++++++++++++++++++----- server/static/auto.js | 17 ++++++++++++----- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/server/app.py b/server/app.py index 66f68c1..a7690d5 100644 --- a/server/app.py +++ b/server/app.py @@ -25,7 +25,7 @@ from capcut_agent.pipeline import ( paste_steps as pipeline_paste_steps, _remap_caps, bg_analyze, bg_draft, bg_steps, _cards_by_cut, _card_paths, ) -from capcut_agent.paste import parse_paste +from capcut_agent.paste import parse_paste, parse_time from capcut_agent.draft import DEFAULT_DRAFT_ROOT, list_drafts, repair_layers from capcut_agent import comments as hlab from capcut_agent import recommend @@ -97,8 +97,28 @@ def _truthy(v: str) -> bool: return str(v).strip().lower() in ("1", "true", "on", "yes") +def _fmt_range_time(sec: float) -> str: + """초 → `M:SS`(1시간 이상은 `H:MM:SS`). youtube.valid_time()이 강제하는 형식과 + 맞춰야 한다 — youtube._fmt_hms()는 밀리초까지 붙어(`H:MM:SS.mmm`) valid_time을 + 통과 못 하므로 여기 전용 포맷 함수를 따로 둔다.""" + sec = max(0, round(sec)) + h, rem = divmod(int(sec), 3600) + m, s = divmod(rem, 60) + return f"{h}:{m:02d}:{s:02d}" if h else f"{m}:{s:02d}" + + def _parse_ranges(raw: str) -> list[tuple[str, str]]: - """JSON [["mm:ss","mm:ss"],...] → [(start,end)]. 빈 값/파싱실패는 [].""" + """JSON [[시작,끝],...] → [(M:SS, M:SS)]. 항목은 "16:07" 문자열/967 같은 초 숫자 + 모두 허용해 parse_time()으로 초로 바꾼 뒤 valid_time()이 통과하는 M:SS/H:MM:SS로 + 재포맷한다. + + ⚠ 함정: 프런트(ytRanges())가 초 숫자를 보내던 시절 이 정규화 없이 그대로 + 저장했다가, 다운로드 경로의 cut_youtube()→valid_time()이 "967" 같은 순수 + 숫자 문자열을 시간 형식으로 인정하지 않아 매 분석이 다운로드 단계에서 + "시간 형식 오류"로 죽었다(구간 탭 전면 불능). 여기서 형식을 통일해두면 + 프런트가 무엇을 보내든(문자열이든 숫자든) 다운로드 경로까지 안전하게 도달한다. + 빈 값/파싱실패 항목은 건너뛴다. + """ raw = (raw or "").strip() if not raw: return [] @@ -109,9 +129,11 @@ def _parse_ranges(raw: str) -> list[tuple[str, str]]: out: list[tuple[str, str]] = [] for item in arr if isinstance(arr, list) else []: if isinstance(item, (list, tuple)) and len(item) == 2: - s, e = str(item[0]).strip(), str(item[1]).strip() - if s and e: - out.append((s, e)) + try: + s_sec, e_sec = parse_time(item[0]), parse_time(item[1]) + except (ValueError, TypeError): + continue + out.append((_fmt_range_time(s_sec), _fmt_range_time(e_sec))) return out diff --git a/server/static/auto.js b/server/static/auto.js index 6e5db5b..2a46ad7 100644 --- a/server/static/auto.js +++ b/server/static/auto.js @@ -935,13 +935,20 @@ function ytSec(v){ return p.length===3?p[0]*3600+p[1]*60+p[2]:p.length===2?p[0]*60+p[1]:p[0]; } function ytRanges(){ + // ⚠ 함정: 여기서 초 숫자로 바꿔 보내면 다운로드 경로(cut_youtube→valid_time)가 + // "MM:SS"/"HH:MM:SS"만 받아들여 매 분석이 다운로드 단계에서 죽는다(구간 탭 + // 전면 불능 — 서버는 방어적으로 재정규화하지만 프런트도 옛 fmtTime 동작대로 + // 문자열 그대로 보내는 게 안전하다). 입력칸은 index.html의 focusout 리스너가 + // 이미 fmtTime()으로 "16:07" 형식을 넣어 두므로 그 문자열을 그대로 쓴다. const out=[]; document.querySelectorAll("#ranges .rng").forEach(row=>{ - const s=ytSec(row.querySelector(".rstart").value); - if(s==null) return; - let e=ytSec(row.querySelector(".rend").value); - if(e==null) e=s+90; // 끝 비면 시작+1:30 (실행 로직과 동일) - if(e>s) out.push([s,e]); + const sv=(row.querySelector(".rstart").value||"").trim(); + const sSec=ytSec(sv); + if(sSec==null) return; + let ev2=(row.querySelector(".rend").value||"").trim(); + let eSec=ytSec(ev2); + if(eSec==null){ eSec=sSec+90; ev2=fmtT(eSec); } // 끝 비면 시작+1:30 (실행 로직과 동일) + if(eSec>sSec) out.push([sv,ev2]); }); return out; }