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:
co-authored by
Claude Opus 4.8
parent
9df7c654ac
commit
91affc5359
-117
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user