township: infer intro role on prompt regen + make prompt drive keyframe

Legacy matches (created before the intro-clips feature) have no role on their
clips, so per-clip prompt regen wrote fight prompts for clips 0-2 instead of
the entrance/entrance/face-off intro. Add _clip_role_fighters() which honours an
explicit role/fighters or infers from position (clip 0 = f1 entrance, clip 1 =
f2 entrance, clip 2 = referee face-off, rest = fight). _fill_clip_prompt() now
uses it and PERSISTS the resolved role/fighters onto the clip so the subsequent
keyframe regen and render apply the correct profiles + LoRAs.

Also make a regenerated prompt authoritative for keyframe generation: clear any
stale kf_prompt override when (re)writing a clip prompt (keyframes compose from
the clip prompt unless an override exists, which would silently win). Same for
outcomes — _plan_outcome_shots now drops o['kf_prompt'] so regenerated outcome
prompts feed the outcome keyframes.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent ba4dedac
...@@ -2236,6 +2236,32 @@ def _build_match_clip_specs(fps: int, cf_lo: int, cf_hi: int, ...@@ -2236,6 +2236,32 @@ def _build_match_clip_specs(fps: int, cf_lo: int, cf_hi: int,
return specs return specs
def _clip_role_fighters(c: dict, f1: str, f2: str):
"""Resolve the (role, fighters) for a fight-match clip.
Honour an explicit `role` (+ `fighters`) when present — that's how new-style
clips built by `_build_match_clip_specs` carry their identity. Otherwise INFER
from position so the first three clips of ANY match (including legacy matches
created before the intro feature, whose clips have no role) are the pre-fight
intro: clip 0 = fighter-1 entrance, clip 1 = fighter-2 entrance, clip 2 = the
referee-officiated face-off, and everything after = the fight."""
role = c.get("role")
if role:
if role == "entrance":
return role, list(c.get("fighters") or [f1])
if role == "faceoff":
return role, list(c.get("fighters") or [f1, f2])
return "fight", [f1, f2]
idx = int(c.get("idx", 0) or 0)
if idx == 0:
return "entrance", [f1]
if idx == 1:
return "entrance", [f2]
if idx == 2:
return "faceoff", [f1, f2]
return "fight", [f1, f2]
def _fill_clip_prompt(prompter, c: dict, f1: str, f2: str, env, env_desc: str, def _fill_clip_prompt(prompter, c: dict, f1: str, f2: str, env, env_desc: str,
char_descriptions: dict, referee: str = None, char_descriptions: dict, referee: str = None,
match_avoid: list = None, focus: str = "") -> str: match_avoid: list = None, focus: str = "") -> str:
...@@ -2244,11 +2270,19 @@ def _fill_clip_prompt(prompter, c: dict, f1: str, f2: str, env, env_desc: str, ...@@ -2244,11 +2270,19 @@ def _fill_clip_prompt(prompter, c: dict, f1: str, f2: str, env, env_desc: str,
role "entrance" → a solo intro for the clip's single fighter; "faceoff" → a role "entrance" → a solo intro for the clip's single fighter; "faceoff" → a
stare-down between both fighters with the referee signalling START; anything stare-down between both fighters with the referee signalling START; anything
else → a normal fight shot (appended to `match_avoid` so the match stays varied). else → a normal fight shot (appended to `match_avoid` so the match stays varied).
""" The resolved role/fighters are PERSISTED onto the clip so the keyframe + render
role = c.get("role", "fight") use the same identity, and any stale `kf_prompt` override is cleared so this
freshly written prompt drives keyframe generation."""
role, fighters = _clip_role_fighters(c, f1, f2)
# Persist the resolved identity (so legacy clips upgraded here keep their role),
# and drop any prior keyframe-prompt override so the new prompt is authoritative.
c["role"] = role
if role in ("entrance", "faceoff"):
c["fighters"] = list(fighters)
c.pop("kf_prompt", None)
cont = _continuity_clause(env or None) cont = _continuity_clause(env or None)
if role == "entrance": if role == "entrance":
who = (c.get("fighters") or [f1])[0] who = fighters[0]
shot = prompter.intro_shot("entrance", who, env_desc=env_desc) shot = prompter.intro_shot("entrance", who, env_desc=env_desc)
hint = _fighter_desc_hint(who, char_descriptions) hint = _fighter_desc_hint(who, char_descriptions)
c["shot"] = shot c["shot"] = shot
...@@ -2591,6 +2625,10 @@ def _plan_outcome_shots(prompter, o: dict, char_descriptions: dict, ...@@ -2591,6 +2625,10 @@ def _plan_outcome_shots(prompter, o: dict, char_descriptions: dict,
o["opponent"] = opponent o["opponent"] = opponent
o["shot"] = shots[0]["shot"] o["shot"] = shots[0]["shot"]
o["prompt"] = shots[0]["prompt"] o["prompt"] = shots[0]["prompt"]
# Freshly written shots carry no per-shot keyframe-prompt override, so the
# outcome keyframes recompose from these new prompts. Also drop any stale
# entry-level override (legacy no-shots path) for the same reason.
o.pop("kf_prompt", None)
def _model_slug(model_id: str) -> str: def _model_slug(model_id: str) -> str:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment