From df8bc7baec3d75ccf61666fea680121cdb8986a7 Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Tue, 4 Aug 2026 11:44:59 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BB=B7=EB=B3=84=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EB=B0=B0=EC=A0=95=20=EC=88=9C=EC=88=98=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=E2=80=94=20quotas=5Ffor,=20build=5Fcut=5Fpicks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 배정 규칙(타임스탐프 우선·중복 제거·Gemini 폴백)을 댓글 수집·HTTP 처리와 분리해 순수 함수로 구현. 기준 카드 길이 3초(CARD_SEC)로 컷별 쿼터 계산. 타임스탠프 매칭은 match_ranges() 이용, Gemini 없이 검증 가능. Co-Authored-By: Claude Opus 5 (1M context) --- capcut_agent/recommend.py | 54 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 capcut_agent/recommend.py diff --git a/capcut_agent/recommend.py b/capcut_agent/recommend.py new file mode 100644 index 0000000..bdecba7 --- /dev/null +++ b/capcut_agent/recommend.py @@ -0,0 +1,54 @@ +"""컷별 댓글 추천 — 자동 탭 검토 화면용. + +왜 별도 모듈인가: 배정 규칙(타임스탬프 우선·중복 제거·모드 분기)은 댓글 수집 +(`comments.py`)과도, HTTP 처리(`server/app.py`)와도 책임이 다르다. 순수 함수로 떼어놔야 +Gemini 없이 단위 검증이 된다. + +설계 근거는 docs/superpowers/specs/2026-08-04-컷별-댓글-추천-design.md. +""" +from __future__ import annotations + +from typing import Dict, List + +from .comments import match_ranges + +# 카드 1장이 차지하는 기준 시간(초). pipeline._load_comment_cards(min_sec) 과 같은 값. +CARD_SEC = 3.0 + + +def quotas_for(cuts) -> List[int]: + """컷별 카드 장수 — max(1, round(컷길이 / CARD_SEC)). 짧은 컷도 최소 1장.""" + return [max(1, round((c["end"] - c["start"]) / CARD_SEC)) for c in cuts] + + +def build_cut_picks(cuts, comments, ai_picks: Dict[int, List[int]], + quotas) -> List[List[dict]]: + """컷별 추천 확정 — 타임스탬프 우선, 남는 자리만 AI, 전 컷 통틀어 중복 금지. + + 타임스탬프가 AI보다 먼저인 이유: 그 컷의 **원본 구간**을 콕 집어 언급한 댓글은 + 근거가 확실하다. 추측(AI)을 이기게 둘 이유가 없다. + 중복은 앞 컷이 가져간다(뒤 컷은 다음 후보로 밀린다). + + ai_picks: {컷인덱스: [댓글idx …]} — Gemini 실패 시 {} 를 넘기면 타임스탬프만으로 채운다. + Returns: 컷별 [{"idx": int, "why": "ts"|"ai"}] + """ + valid = {c["idx"] for c in comments} + used: set = set() + out: List[List[dict]] = [] + for i, cut in enumerate(cuts): + quota = quotas[i] if i < len(quotas) else 0 + picks: List[dict] = [] + for idx in match_ranges(comments, [(cut["start"], cut["end"])]): + if len(picks) >= quota: + break + if idx not in used: + picks.append({"idx": idx, "why": "ts"}) + used.add(idx) + for idx in (ai_picks.get(i) or []): + if len(picks) >= quota: + break + if idx in valid and idx not in used: + picks.append({"idx": idx, "why": "ai"}) + used.add(idx) + out.append(picks) + return out