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; }