diff --git a/backend/main.py b/backend/main.py index ddd2c87..96dd9e2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -503,6 +503,7 @@ def handle_suggest_clips(task_id: str, params: dict): segments = params.get("segments", []) top_n = params.get("top_n", 5) + existing_clips = params.get("existing_clips", []) if not segments: emit_result(task_id, "error", error="segments is required") @@ -522,6 +523,7 @@ def handle_suggest_clips(task_id: str, params: dict): clips = suggest_with_claude( segments=segments, top_n=top_n, + exclude_clips=existing_clips, progress_callback=lambda pct, msg: emit_progress(task_id, "suggesting", pct, msg), ) diff --git a/backend/services/claude_suggest.py b/backend/services/claude_suggest.py index ad9a5d6..3bfcd6f 100644 --- a/backend/services/claude_suggest.py +++ b/backend/services/claude_suggest.py @@ -684,6 +684,30 @@ def _dedupe_clips_by_range(clips: list[dict]) -> list[dict]: return sorted(kept, key=lambda c: c.get("start_second", 0)) +def _drop_clips_overlapping(clips: list[dict], exclude_clips: list[dict]) -> list[dict]: + """Drop clips that overlap an excluded range by >50% of the shorter clip. + The prompt already asks the AI to skip these; this enforces it if it doesn't.""" + if not exclude_clips: + return clips + kept = [] + for clip in clips: + start = float(clip.get("start_second", 0)) + end = float(clip.get("end_second", 0)) + dur = max(0.0, end - start) + overlaps = False + for ex in exclude_clips: + ex_start = float(ex.get("start_second", 0)) + ex_end = float(ex.get("end_second", 0)) + overlap = max(0.0, min(end, ex_end) - max(start, ex_start)) + shorter = min(dur, max(0.0, ex_end - ex_start)) or 1.0 + if overlap / shorter > 0.5: + overlaps = True + break + if not overlaps: + kept.append(clip) + return kept + + def _select_top_by_score(clips: list[dict], top_n: int) -> list[dict]: """Keep the highest-scored `top_n` clips, then order them by start time. Ranking by score must come before truncation — otherwise the earliest clips @@ -1014,7 +1038,9 @@ def _parse_seconds(val) -> float: "_ai_engine": engine, }) - selected = _select_top_by_score(normalized, top_n) + selected = _select_top_by_score( + _drop_clips_overlapping(normalized, exclude_clips or []), top_n + ) if selected: if progress_callback: diff --git a/package.json b/package.json index 202d662..8a26d59 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "podcli", - "version": "2.2.1", + "version": "2.3.1", "description": "AI-powered podcast clip generator for TikTok/YouTube Shorts. Transcribe, find viral moments, export vertical clips with burned captions.", "type": "module", "license": "AGPL-3.0-only", diff --git a/src/ui/client/ClipDetail.tsx b/src/ui/client/ClipDetail.tsx index 6dd5474..ee52fbb 100644 --- a/src/ui/client/ClipDetail.tsx +++ b/src/ui/client/ClipDetail.tsx @@ -247,7 +247,7 @@ export default function ClipDetail() {