From 91affc53597b0e36c13b26ac0839795e59f3cba2 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:20:28 +0300 Subject: [PATCH] Stage 1 cleanup: delete dead splitter method; preview uses split_scenes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generation now reads split_scenes, so the duplicate _v2_split_prompt_by_transitions method has zero callers — deleted. bridge.parse_timeline (the 'what Studio sees' preview) now derives from split_scenes too, so the preview's scene count/boundaries match generation exactly (no spurious 'split produced N but timeline has M' warnings). The lossless RAW editing split (parse_timeline_verbatim / split_timeline_verbatim) stays separate — it has a genuinely different lossless requirement (round-trips the exact typed text). It still uses the old offset-projection path; converting it to a lossless piece-walker is the one remaining Stage-1 loose end. Tests: 33 + 140 pass. Co-Authored-By: Claude Opus 4.8 --- conditioning.py | 117 --------------------------------- movie_editor/backend/bridge.py | 17 +++-- 2 files changed, 12 insertions(+), 122 deletions(-) diff --git a/conditioning.py b/conditioning.py index 17643b0c..3a1e5cc8 100644 --- a/conditioning.py +++ b/conditioning.py @@ -6728,123 +6728,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): ) return cond, meta, encode_status, "CONDITIONING-owned" - def _v2_split_prompt_by_transitions(self, prompt, placement="start"): - """Split prompt at transition words. - - placement='start': transition word goes to the START of the new segment (default). - placement='end': transition word goes to the END of the previous segment. - - Custom transitions can override placement per-entry regardless of the global setting. - - Returns a list of (text, visual_effect) tuples with >= 1 entry. - visual_effect is the effect that fires BEFORE this segment (None for first segment). - Single-entry list means no transitions were found. - """ - text = str(prompt or "").strip() - if not text: - return [(text, None)] - - try: - from .templates import load_custom_transition_triggers - except ImportError: - from templates import load_custom_transition_triggers - custom_map = load_custom_transition_triggers() # {trigger: {"placement": ..., "visual_effect": ...}} - custom_triggers = list(custom_map.keys()) - - if custom_triggers: - def _custom_trigger_pat(t): - end = r"\b" if re.search(r"\w$", t) else r"(?=\s|$)" - return r"\b" + re.escape(t) + end - custom_parts = "|".join( - _custom_trigger_pat(t) - for t in sorted(custom_triggers, key=len, reverse=True) - ) - split_pattern = re.compile( - r"(?:" + custom_parts + r"|" + _GENERIC_SCENE_LABEL_PATTERN + r")", - re.IGNORECASE, - ) - else: - split_pattern = re.compile(_GENERIC_SCENE_LABEL_PATTERN, re.IGNORECASE) - - # Capturing split keeps the matched transition words as alternating elements: - # [pre, trans1, between, trans2, post, ...] - capturing_pattern = re.compile(r"(" + split_pattern.pattern + r")", re.IGNORECASE) - parts = capturing_pattern.split(text) - segments = [] # list of segment text strings - effects = [] # parallel list: visual_effect that started each segment (None for segment 0) - # parts[0] = text before first transition; then pairs of (trans_word, following_text) - first_segment = parts[0].strip().strip(",;.:").strip() - starts_with_transition = not first_segment - if starts_with_transition: - segments.append("") - effects.append(None) - current = first_segment - current_effect = None # effect that triggered the current pending segment - i = 1 - while i < len(parts): - trans_word = parts[i].strip() - following = parts[i + 1] if i + 1 < len(parts) else "" - following = following.strip().lstrip(",;.:").strip() - # per-trigger placement override takes priority over global setting - entry = custom_map.get(trans_word.lower()) or {} - trans_placement = entry.get("placement") or placement if isinstance(entry, dict) else placement - trans_visual_effect = entry.get("visual_effect", "none") if isinstance(entry, dict) else "none" - if trans_visual_effect == "none": - trans_visual_effect = None - if trans_placement == "end": - segments.append((current + " " + trans_word).strip().strip(",;.:").strip() if current else trans_word) - effects.append(current_effect) - current = following - current_effect = trans_visual_effect - elif trans_placement == "silent": - if current: - segments.append(current) - effects.append(current_effect) - current = following - current_effect = trans_visual_effect - else: - if current: - segments.append(current) - effects.append(current_effect) - current = re.sub(r"\b(a|an|the),\s*", r"\1 ", (trans_word + " " + following).strip().strip(",;.:").strip(), flags=re.IGNORECASE).strip() - current_effect = trans_visual_effect - i += 2 - if current: - segments.append(current) - effects.append(current_effect) - - # Stacking fix: if a segment consists entirely of transition markers with no real content, - # two transition phrases were adjacent ("after a brief scene cut, in the next scene ..."). - # Prepend the pure-transition segment to the FOLLOWING segment so they read as one unit. - def _real_content(seg): - s = split_pattern.sub("", seg).strip().strip(",;.: ") - # lone articles ("a", "an", "the") left behind by a dangling transition are not real content - return re.sub(r"^(a|an|the)\s*$", "", s, flags=re.IGNORECASE).strip() - - i = 0 - while i < len(segments): - if segments[i] == "": - i += 1 - continue - if not _real_content(segments[i]) and i + 1 < len(segments): - connector = " " if re.search(r"\b(a|an|the)\s*$", segments[i], re.IGNORECASE) else ", " - segments[i + 1] = (segments[i] + connector + segments[i + 1]).strip() - # keep the effect of the segment with real content (i+1), discard i's effect - segments.pop(i) - effects.pop(i) - else: - i += 1 - - # Drop a trailing segment that is only a transition phrase plus an article - it means - # the prompt ended with a dangling transition ("...cuts to the") with no scene after it. - if len(segments) > 1 and not _real_content(segments[-1]): - segments.pop() - effects.pop() - - if len(segments) > 1: - return list(zip(segments, effects)) - return [(text, None)] - def _v2_transition_scene_texts(self, segments): """Accept list of (text, effect) tuples. Return (scene_texts, scene_effects).""" if not segments: diff --git a/movie_editor/backend/bridge.py b/movie_editor/backend/bridge.py index 1c6afce4..55478a3c 100644 --- a/movie_editor/backend/bridge.py +++ b/movie_editor/backend/bridge.py @@ -54,11 +54,18 @@ def _funpack_imports(): def parse_timeline(prompt: str, seed: int = 0) -> dict: - """{anchor, scenes, transitions} — the canonical split Studio will see. - Shortcuts are expanded first (as generation does).""" - parse_timeline_segments, apply_prompt_shortcuts, *_ = _funpack_imports() - expanded, _applied = apply_prompt_shortcuts(str(prompt or ""), seed=int(seed or 0)) - return parse_timeline_segments(expanded) + """{anchor, scenes, transitions} — the canonical split Studio will see, via the ONE splitter + (split_scenes) generation now uses, so the preview's scene count/boundaries match generation + exactly. Scene text is the EXPANDED text (what Studio encodes); the anchor is prepended by + Studio per scene. The lossless raw editing split lives in parse_timeline_verbatim.""" + split_fn = _funpack_attr("conditioning", "split_scenes") + s = split_fn(str(prompt or "")) + scenes = [{"index": i, "text": (sc.get("expanded") or "")} + for i, sc in enumerate(s.get("scenes", []) or [])] + transitions = [{"after_scene": i - 1, "visual_effect": sc.get("effect")} + for i, sc in enumerate(s.get("scenes", []) or []) + if i > 0 and sc.get("effect")] + return {"anchor": s.get("anchor_expanded", ""), "scenes": scenes, "transitions": transitions} def parse_timeline_raw(prompt: str) -> dict: