Stage 1 cleanup: delete dead splitter method; preview uses split_scenes

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 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-06-15 13:20:28 +03:00
co-authored by Claude Opus 4.8
parent 9df7c654ac
commit 91affc5359
2 changed files with 12 additions and 122 deletions
-117
View File
@@ -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:
+12 -5
View File
@@ -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: