township: apply correct per-segment LoRAs/profiles at outcome render time

The outcome video render applied one fighter list (both match fighters) to the
whole clip, so the referee LoRA and winner-only identity only existed in the
keyframe, not at video-generation time. Thread per-segment fighters through
_render: segments are now (prompt, frames, seg_stem, seg_fighters) and each
segment (and each chained part) applies exactly its own character_profiles +
LoRAs, overriding the clip-level list.

Outcome segments now load: FINISH = both fighters; VICTORY = winner + referee
(decisive) or both fighters + referee (draw). Referee resolution matches the
keyframe path. Backward compatible — shorter segment tuples and legacy clips
without role/fighters fall back to the clip-level fighters.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent e28f11e9
...@@ -3598,37 +3598,45 @@ def _stage_videos_render(client, video_model, video_dir, fight_plan, outcome_pla ...@@ -3598,37 +3598,45 @@ def _stage_videos_render(client, video_model, video_dir, fight_plan, outcome_pla
one continuous shot and discarded, so callers (and the Matches page) still one continuous shot and discarded, so callers (and the Matches page) still
see exactly one file per planned clip. Returns (ok, duration, fatal). see exactly one file per planned clip. Returns (ok, duration, fatal).
`segments` (optional) is a list of (prompt, frames[, seg_stem]) describing a `segments` (optional) is a list of (prompt, frames[, seg_stem[, seg_fighters]])
SEQUENCE of distinct shots to render back-to-back into one continuous video — describing a SEQUENCE of distinct shots to render back-to-back into one
e.g. an outcome's finish clip then its victory clip. Each segment uses its continuous video — e.g. an outcome's finish clip then its victory clip. Each
OWN prompt verbatim (a deliberate new action). When a segment carries its own segment uses its OWN prompt verbatim (a deliberate new action). When a segment
keyframe stem (seg_stem), that segment is ANCHORED to its own keyframe image carries its own keyframe stem (seg_stem), that segment is ANCHORED to its own
(a clean cut into a new, correctly-composed shot — e.g. the referee raising keyframe image (a clean cut into a new, correctly-composed shot — e.g. the
the winner's arm); otherwise it seeds from the previous shot's last frame / referee raising the winner's arm); otherwise it seeds from the previous shot's
VACE tail for visual continuity. When omitted, the single (prompt, nf) is last frame / VACE tail for visual continuity. When a segment carries its own
rendered as before.""" `seg_fighters` list, that segment applies exactly those character profiles +
LoRAs (e.g. the FINISH shows both fighters, while the VICTORY shows only the
winner + the referee). When omitted, the single (prompt, nf) is rendered as
before."""
keyframe = _keyframe_bytes(stem) if stem else None keyframe = _keyframe_bytes(stem) if stem else None
# Build a flat list of parts: (segment_prompt, frames, is_segment_start, # Build a flat list of parts: (segment_prompt, frames, is_segment_start,
# segment_keyframe_bytes). The segment keyframe (if any) anchors that # segment_keyframe_bytes, segment_fighters). The segment keyframe (if any)
# segment's first part to its own image instead of chaining from the prior. # anchors that segment's first part to its own image instead of chaining from
# the prior; the segment fighters (if any) override the clip-level profiles +
# LoRAs for that segment.
if segments: if segments:
seglist = [] seglist = []
for seg in segments: for seg in segments:
sp, sn = seg[0], seg[1] sp, sn = seg[0], seg[1]
sstem = seg[2] if len(seg) > 2 else None sstem = seg[2] if len(seg) > 2 else None
sfighters = seg[3] if len(seg) > 3 else None
if int(sn) > 0: if int(sn) > 0:
seglist.append((str(sp), int(sn), sstem)) seglist.append((str(sp), int(sn), sstem, sfighters))
else: else:
seglist = [(prompt, int(nf), stem)] seglist = [(prompt, int(nf), stem, None)]
parts_plan = [] parts_plan = []
for sp, sn, sstem in seglist: for sp, sn, sstem, sfighters in seglist:
seg_kf = _keyframe_bytes(sstem) if sstem else None seg_kf = _keyframe_bytes(sstem) if sstem else None
for bi, bn in enumerate(_split_frame_budget(sn, _chunk_max)): for bi, bn in enumerate(_split_frame_budget(sn, _chunk_max)):
parts_plan.append((sp, bn, bi == 0, seg_kf)) parts_plan.append((sp, bn, bi == 0, seg_kf, sfighters))
if len(parts_plan) == 1: if len(parts_plan) == 1:
sp, bn, _, seg_kf = parts_plan[0] sp, bn, _, seg_kf, sfighters = parts_plan[0]
return _render_once(label, sp, profiles, env, bn, out_path, _pf = sfighters if sfighters is not None else profiles
fighters=fighters, init_override=(seg_kf or keyframe), _ff = sfighters if sfighters is not None else fighters
return _render_once(label, sp, _pf, env, bn, out_path,
fighters=_ff, init_override=(seg_kf or keyframe),
step_cb=step_cb) step_cb=step_cb)
# Chained multi-part shot: part 0 starts from the clip keyframe; each later # Chained multi-part shot: part 0 starts from the clip keyframe; each later
# part is seeded by the previous part's last frame → seamless single take. # part is seeded by the previous part's last frame → seamless single take.
...@@ -3649,7 +3657,7 @@ def _stage_videos_render(client, video_model, video_dir, fight_plan, outcome_pla ...@@ -3649,7 +3657,7 @@ def _stage_videos_render(client, video_model, video_dir, fight_plan, outcome_pla
tmpd = tempfile.mkdtemp(prefix="twshot_") tmpd = tempfile.mkdtemp(prefix="twshot_")
parts, prev_last, prev_tail = [], None, None parts, prev_last, prev_tail = [], None, None
try: try:
for pi, (seg_prompt, pn, seg_start, seg_kf) in enumerate(parts_plan): for pi, (seg_prompt, pn, seg_start, seg_kf, seg_fighters) in enumerate(parts_plan):
part_path = os.path.join(tmpd, f"part{pi:02d}.mp4") part_path = os.path.join(tmpd, f"part{pi:02d}.mp4")
# Tag each part's step updates with part N/total so the UI can show # Tag each part's step updates with part N/total so the UI can show
# "concatenating shot — part 2/3" alongside the diffusion step. # "concatenating shot — part 2/3" alongside the diffusion step.
...@@ -3687,10 +3695,14 @@ def _stage_videos_render(client, video_model, video_dir, fight_plan, outcome_pla ...@@ -3687,10 +3695,14 @@ def _stage_videos_render(client, video_model, video_dir, fight_plan, outcome_pla
part_prompt = ( part_prompt = (
"Continuing seamlessly from the previous moment, the action keeps " "Continuing seamlessly from the previous moment, the action keeps "
"moving FORWARD — new movement that advances the moment. " + seg_prompt) "moving FORWARD — new movement that advances the moment. " + seg_prompt)
# Per-segment profiles + LoRAs (e.g. victory shot = winner + referee)
# override the clip-level ones for this part when provided.
_pf = seg_fighters if seg_fighters is not None else profiles
_ff = seg_fighters if seg_fighters is not None else fighters
ok, _dur, is_fatal = _render_once( ok, _dur, is_fatal = _render_once(
f"{label} [part {pi+1}/{_nparts}, {pn}f]", f"{label} [part {pi+1}/{_nparts}, {pn}f]",
part_prompt, profiles, env, pn, part_path, part_prompt, _pf, env, pn, part_path,
fighters=fighters, init_override=seed_img, step_cb=_pcb, fighters=_ff, init_override=seed_img, step_cb=_pcb,
cond_frames=cond_frames) cond_frames=cond_frames)
if not ok: if not ok:
return False, None, is_fatal return False, None, is_fatal
...@@ -3856,13 +3868,32 @@ def _stage_videos_render(client, video_model, video_dir, fight_plan, outcome_pla ...@@ -3856,13 +3868,32 @@ def _stage_videos_render(client, video_model, video_dir, fight_plan, outcome_pla
# outcomes (no shots) fall back to the one-prompt path. # outcomes (no shots) fall back to the one-prompt path.
_oshots = o.get("shots") or None _oshots = o.get("shots") or None
_segments = None _segments = None
# Referee for this match's victory shot (prefer the chosen one; fall back to
# any available referee profile), and whether this is the (both-arms) draw.
_oref = (_mref_map.get(o.get("match_name"))
or _referee_for(_out_dir, o.get("match_name") or o.get("fighter", "")))
_o_is_draw = o.get("outcome") == "draw"
_victory_roles = {"victory", "draw_decision"}
if _oshots: if _oshots:
# Each segment carries its OWN keyframe stem (finish, then victory) so # Each segment carries its OWN keyframe stem (finish, then victory) so
# the render anchors every outcome clip to its dedicated image. # the render anchors every outcome clip to its dedicated image, AND its
_segments = [(s.get("prompt") or o["prompt"], int(s.get("nf") or 0), # OWN fighters so the right profiles + LoRAs apply: the FINISH shows both
_outcome_seg_stem(clip_name, si)) # fighters; the VICTORY shows only the winner (+ referee) — or both
for si, s in enumerate(_oshots) if int(s.get("nf") or 0) > 0] # fighters (+ referee) for a draw.
_onf = sum(n for _, n, _ in _segments) or _onf _segments = []
for si, s in enumerate(_oshots):
_snf = int(s.get("nf") or 0)
if _snf <= 0:
continue
if s.get("role") in _victory_roles:
_sf = list(_ofighters) if _o_is_draw else [o["fighter"]]
if _oref and _oref not in _sf:
_sf.append(_oref)
else:
_sf = list(_ofighters)
_segments.append((s.get("prompt") or o["prompt"], _snf,
_outcome_seg_stem(clip_name, si), _sf))
_onf = sum(n for _, n, _, _ in _segments) or _onf
_roles = " → ".join(s.get("role", "?") for s in _oshots) _roles = " → ".join(s.get("role", "?") for s in _oshots)
_log(f" ({len(_segments)}-shot sequence: {_roles})") _log(f" ({len(_segments)}-shot sequence: {_roles})")
ok, dur, is_fatal = _render( ok, dur, is_fatal = _render(
......
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