Add configurable Planner prompt trigger with motion_transfer default (1.115.8)
This commit is contained in:
+16
-11
@@ -2111,8 +2111,8 @@ def _clean_written(text: str) -> str:
|
||||
# user's request (the prompt box) and the shot's marks and mode.
|
||||
|
||||
# frames 0 = from the shot's length; style: "h3" (the official six sections) or "captions" (the short OpenJutsu captions)
|
||||
AUTO_PROMPT_DEFAULTS = {"enabled": False, "frames": 0, "source": "guide", "style": "h3"}
|
||||
AUTO_PROMPT_REVISION = "motion-isolated-picture-bindings-v4"
|
||||
AUTO_PROMPT_DEFAULTS = {"enabled": False, "frames": 0, "source": "guide", "style": "h3", "trigger": "motion_transfer"}
|
||||
AUTO_PROMPT_REVISION = "motion-trigger-bindings-v5"
|
||||
|
||||
AUTO_REQUEST_RULES = """Motion-transfer contract and requested visual changes (apply in every output format):
|
||||
- The source performance is fixed: transfer the assigned performer's actions, body movement, gesture timing, head motion, gaze, visible mouth movement and expressions at the corresponding timestamps. Retarget that performance to the reference character's anatomy.
|
||||
@@ -2187,16 +2187,16 @@ def shot_mode(seg: dict) -> str:
|
||||
return "mask" if seg.get("inpaint") else "paste" if seg.get("paste") else "frame"
|
||||
|
||||
|
||||
def auto_prompt_for(instruction: str, names: list, seg: dict, template: str = "", style: str = "h3") -> str:
|
||||
def auto_prompt_for(instruction: str, names: list, seg: dict, template: str = "", style: str = "h3", prompt_trigger: str = "motion_transfer") -> str:
|
||||
"""What a written prompt was made from (the panel computes the same string to show it is out of date)."""
|
||||
marks = [[x["picture"], x["color"]] for x in seg.get("color_guide_marks") or []] if seg.get("color_guide") else []
|
||||
return json.dumps([instruction or "", names, marks, shot_mode(seg), (seg.get("mask") or {}).get("text") or "",
|
||||
seg.get("target") or "", template or "", style or "h3", AUTO_PROMPT_REVISION], separators=(",", ":"), ensure_ascii=False)
|
||||
seg.get("target") or "", template or "", style or "h3", prompt_trigger, AUTO_PROMPT_REVISION], separators=(",", ":"), ensure_ascii=False)
|
||||
|
||||
|
||||
def write_h3_prompt(clip, frames: list, times: list, pictures: list, picture_texts: list, instruction: str,
|
||||
seg: dict, source: str = "guide", duration: float = 0.0, max_tokens: int = 2048,
|
||||
cuts: list | None = None, template: str = "", style: str = "h3", source_facts: str | None = None) -> str:
|
||||
cuts: list | None = None, template: str = "", style: str = "h3", source_facts: str | None = None, prompt_trigger: str = "motion_transfer") -> str:
|
||||
"""One shot's prompt in H3's full-reference format, written by the VLM from key frames of the shot ([1,H,W,3] each,
|
||||
at `times` seconds into it), the shot's reference pictures (<Picture 1> first) with their descriptions, the user's
|
||||
request and the shot's Colored Marks, selection and mode."""
|
||||
@@ -2243,7 +2243,9 @@ def write_h3_prompt(clip, frames: list, times: list, pictures: list, picture_tex
|
||||
# the template: the conditioning prompt input (the one a LoRA was trained with), or a request that is a whole prompt
|
||||
if not template and is_template(instruction):
|
||||
template, instruction = instruction, ""
|
||||
trigger = request_trigger(template)
|
||||
trigger = request_trigger(template) or request_trigger(instruction) or str(prompt_trigger or "").strip().rstrip(":")
|
||||
if trigger and not re.fullmatch(r"[A-Za-z][\w-]*", trigger):
|
||||
raise ValueError("Invalid prompt trigger")
|
||||
if cuts:
|
||||
shots_rule = ("The source has camera cuts at " + ", ".join(_mmss(t) for t in cuts) + ": start a new shot at each "
|
||||
"of them and nowhere else, " + ", ".join(f'"[Shot {k + 2}] At {_mmss(t)}, the shot cuts to ..."'
|
||||
@@ -2290,7 +2292,7 @@ def write_h3_prompt(clip, frames: list, times: list, pictures: list, picture_tex
|
||||
images = _letterbox_batch(list(frames) + list(pictures))
|
||||
tokens = 512 if captions else max(1536, int(max_tokens))
|
||||
sampling = {"temperature": 0.6, "repetition_penalty": 1.1, "presence_penalty": 0.8}
|
||||
tidy = (lambda t: tidy_caption(t, seg)) if captions else \
|
||||
tidy = (lambda t: tidy_caption(t, seg, trigger)) if captions else \
|
||||
(lambda t: tidy_h3_prompt(t, seg, trigger, bool(cuts), npics, bool(template)))
|
||||
check = (lambda t: caption_problems(t, npics)) if captions else (lambda t: h3_prompt_problems(t, npics, bool(template)))
|
||||
text = tidy(vlm_generate(clip, ask, images, tokens, sampling=sampling))
|
||||
@@ -2304,7 +2306,7 @@ def write_h3_prompt(clip, frames: list, times: list, pictures: list, picture_tex
|
||||
return text if captions else name_subjects(text)
|
||||
|
||||
|
||||
def tidy_caption(text: str, seg: dict | None = None) -> str:
|
||||
def tidy_caption(text: str, seg: dict | None = None, trigger: str | None = None) -> str:
|
||||
"""A caption-style answer: one paragraph, no fences or quotes, no repeated sentences, labels with brackets, and
|
||||
the Colored Mark mapping added when a mark color is missing."""
|
||||
t = str(text or "").replace("```text", "").replace("```", "").strip().strip('"').strip()
|
||||
@@ -2312,6 +2314,9 @@ def tidy_caption(text: str, seg: dict | None = None) -> str:
|
||||
t = re.sub(r"(?<![<\w])(Picture) (\d+)(?![\w>])", r"<\1 \2>", t)
|
||||
if seg and seg.get("color_guide"):
|
||||
t = color_guide_prompt(t, seg.get("color_guide_marks") or [])
|
||||
if trigger is not None:
|
||||
t = re.sub(r"^(?:motion_transfer|body_swap|vfx_edit):\s*", "", t, flags=re.I)
|
||||
if trigger:t = trigger + ": " + t
|
||||
return " ".join(t.split())
|
||||
|
||||
|
||||
@@ -2488,8 +2493,8 @@ def auto_prompt_shot(clip, plan: dict, analysis: dict, path: str, seg: dict, nam
|
||||
text = write_h3_prompt(clip, frames, [(int(x) - seg["start"]) / fps for x in picks], pictures,
|
||||
picture_evidence, request, seg, ap.get("source") or "guide", L / fps,
|
||||
int(vcfg.get("max_tokens") or 1024), [(c - seg["start"]) / fps for c in cuts], tmpl,
|
||||
ap.get("style") or "h3", source_facts=source_facts)
|
||||
return {"text": text, "for": auto_prompt_for(instruction, names, seg, template, ap.get("style") or "h3")}
|
||||
ap.get("style") or "h3", source_facts=source_facts, prompt_trigger=ap["trigger"])
|
||||
return {"text": text, "for": auto_prompt_for(instruction, names, seg, template, ap.get("style") or "h3", ap["trigger"])}
|
||||
|
||||
|
||||
PLACE_Q = ("This picture shows a place; the patch of black-and-white noise only hides a person, ignore it. Describe the "
|
||||
@@ -3494,7 +3499,7 @@ class BFSShotPlanner:
|
||||
got = stored.get(key) or {}
|
||||
if got.get("text"):
|
||||
written[i] = got["text"]
|
||||
if got.get("for") != auto_prompt_for(request, names_of(segs[i]), segs[i], template, ap["style"]):
|
||||
if got.get("for") != auto_prompt_for(request, names_of(segs[i]), segs[i], template, ap["style"], ap["trigger"]):
|
||||
print(f"[BFS Shot Planner] shot {i + 1}: its written prompt is older than its request, pictures or "
|
||||
"marks (🤖 Write in the panel to write it again)", flush=True)
|
||||
elif vlm is not None:
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "bfsnodes"
|
||||
description = "Custom ComfyUI nodes for reference-guided video: BFS Sampler (guide-driven IC-LoRAs, auto crop, mask inpainting, temporal chunking), LTX Multiple Controls / Identity Transfer / Edit Anything, Head-Swap and Anime2Real conditioning, MiniMax-H3 latent guides, RoPE layouts and loaders, LoRA Surgery, and Shot Planner / Shot Loop (tabbed panel): split long videos into shots at camera cuts, face-tracked cast, SAM 3 mask per shot with full frame, paste, mask, crop and crop + mask generation modes, optional translucent Colored Mark overlay for manually selected regions, optional Qwen3-VL to suggest settings, describe references ({details}) and the person to replace ({target}), continuity between shots, H3 conditioning with an optional duet and prompt writer, join with the original timing, audio and a side-by-side comparison. H3 Duet: pin a source clip beside the video, training-free."
|
||||
version = "1.115.7"
|
||||
version = "1.115.8"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = [
|
||||
"torch",
|
||||
|
||||
@@ -1074,7 +1074,7 @@ class MultiRefTest(unittest.TestCase):
|
||||
self.assertNotIn("anime", seen["prompt"].lower()) # no sample style may leak into a request with no style edit
|
||||
self.assertNotIn("Example request:", seen["prompt"])
|
||||
self.assertEqual(seen["tokens"], 512)
|
||||
self.assertTrue(out.startswith("Replace the dog in the video with the gaunt goblin from <Picture 1>."))
|
||||
self.assertTrue(out.startswith("motion_transfer: Replace the dog in the video with the gaunt goblin from <Picture 1>."))
|
||||
self.assertEqual(SL.caption_problems(out, 2), [])
|
||||
self.assertTrue(SL.caption_problems("subject_definitions: <Subject 1> is x", 1))
|
||||
|
||||
@@ -1678,3 +1678,9 @@ class IsolatedReferenceBindingTests(unittest.TestCase):
|
||||
start = browser.index("const autoFor =")
|
||||
signature = browser[start:browser.index("const writtenOf", start)]
|
||||
self.assertIn('"' + SL.AUTO_PROMPT_REVISION + '"', signature)
|
||||
|
||||
def test_motion_trigger_is_added_once_and_can_be_disabled(self):
|
||||
self.assertEqual(SL.tidy_caption("Replace the actor.", trigger="motion_transfer"), "motion_transfer: Replace the actor.")
|
||||
self.assertEqual(SL.tidy_caption("motion_transfer: Replace the actor.", trigger="motion_transfer"), "motion_transfer: Replace the actor.")
|
||||
self.assertEqual(SL.tidy_caption("motion_transfer: Replace the actor.", trigger=""), "Replace the actor.")
|
||||
self.assertNotEqual(SL.auto_prompt_for("edit", [], {}, prompt_trigger=""), SL.auto_prompt_for("edit", [], {}, prompt_trigger="motion_transfer"))
|
||||
|
||||
@@ -527,7 +527,7 @@ const meta = i => { while (plan.segs.length <= i) plan.segs.push({}); return pla
|
||||
const marks = s.color_guide ? [[s.color_guide_picture, pictureColor(plan, s.color_guide_picture)],
|
||||
...s.regions.filter(r => String(r.text || "").trim() || (r.points || []).length).map(r => [r.picture, pictureColor(plan, r.picture)])] : [];
|
||||
return JSON.stringify([s.prompt || plan.global_prompt || "", s.names, marks, modeOf(s), s.mask?.text || "", s.target || "",
|
||||
plan.auto_prompt?.template || "", plan.auto_prompt?.style || "h3", "motion-isolated-picture-bindings-v4"]);
|
||||
plan.auto_prompt?.template || "", plan.auto_prompt?.style || "h3", plan.auto_prompt?.trigger ?? "motion_transfer", "motion-trigger-bindings-v5"]);
|
||||
};
|
||||
const writtenOf = s => plan.auto_prompts?.[segKey(s)] || null;
|
||||
const setAuto = (k, v) => { plan.auto_prompt = { ...plan.auto_prompt, [k]: v }; save(); };
|
||||
|
||||
@@ -118,6 +118,8 @@ function autoSection(c) {
|
||||
"On: the prompt boxes (global and per shot) hold your request ('change the bottle into a red can', 'Picture 1 dances in the rain'); the VLM writes each shot's MiniMax H3 prompt from key frames of the shot, the pictures and their descriptions, the request and the shot's Colored Marks, selection and mode, in H3's official full-reference format."),
|
||||
fld("Style", select(A.style || "h3", [["h3", "H3 official (six sections)"], ["captions", "OpenJutsu captions (short)"]],
|
||||
v => c.setAuto("style", v), { style: "width:auto" }), "Choose prompt format. The guide performance stays fixed; request style, setting, lighting, outfit or camera changes explicitly. Identify setting pictures in the request; bind colors only to character references."),
|
||||
fld("Trigger", select(A.trigger ?? "motion_transfer", [["motion_transfer", "motion_transfer:"], ["body_swap", "body_swap:"], ["", "None (plain prompt)"]],
|
||||
v => c.setAuto("trigger", v), { style: "width:auto" }), "Match the LoRA caption prefix. Choose None for a model trained without a trigger."),
|
||||
fld("Key frames", select(String(A.frames || 0), [["0", "auto (by length)"], ...[3, 4, 5, 6, 8, 10, 12].map(n => [String(n), String(n)])],
|
||||
v => c.setAuto("frames", parseInt(v) || 0), { style: "width:auto" }), "auto: about 1.5 per second, 3 to 8"),
|
||||
fld("Source video in the prompt", select(A.source || "guide", [["guide", "guide video (aligned guide)"], ["video", "<Video 1> (native reference video)"]],
|
||||
|
||||
Reference in New Issue
Block a user