Keep multiline beat prompts intact

This commit is contained in:
2026-09-02 13:00:25 +00:00
parent 271e7de300
commit 08630964d5
2 changed files with 81 additions and 57 deletions
+20 -35
View File
@@ -317,34 +317,25 @@ def expand_beats(paras, mode="auto"):
(beats, note). (beats, note).
Beats are separated by a BLANK line (or a '##' line). That is unambiguous, but Beats are separated by a BLANK line (or a '##' line). That is unambiguous, but
it is also the single easiest thing to get wrong in a textarea: six beats typed it is also easy to misread once a dedicated beat editor exists: inside the
on six consecutive lines are one paragraph, so they render as ONE shot with six beat editor, one textbox is one beat, so plain newlines inside that textbox
actions crammed into it -- which reads as characters moving at triple speed, not should stay inside the same beat rather than quietly creating new shots.
as a splitting problem.
mode: mode:
'auto' -- blank lines first; any paragraph still holding more than one 'auto' -- preserve each paragraph as one beat.
content line is then split one beat per line, and says so. 'each line' -- every content line is its own beat, with directive lines
'each line' -- every content line is its own beat. Same result as 'auto'; kept attaching to the beat they configure.
so the intent can be stated explicitly.
There is deliberately no strict blank-lines-only mode any more. It was the ONE
setting that could silently lose beats: six beats typed as two blocks of three
rendered as two shots, with no note to say why, because the split note is only
written when a paragraph is actually split. Nothing else on the node can change
the beat count, so removing that option removes the whole failure class. A
workflow that still stores 'blank line' falls through to 'auto' below.
Directive lines ('wardrobe:', 'seconds:', 'exit:' ...) are never beats of their Directive lines ('wardrobe:', 'seconds:', 'exit:' ...) are never beats of their
own: they attach to the next content line, or to the previous beat if they own: they attach to the next content line, or to the previous beat if they
trail the paragraph.""" trail the paragraph."""
# Any unrecognized mode means AUTO, never "do nothing". An earlier version fell # Any unrecognized mode means AUTO, never "do nothing". Older workflows may
# through an if/elif with no else and silently DROPPED every multi-line paragraph # still store removed values such as 'blank line'; those should preserve whole
# -- six beats arrived as two shots with four beats simply gone. A stale value on # paragraphs instead of unexpectedly splitting every newline into a beat.
# this widget (including 'blank line' from a workflow saved before it was removed)
# is enough to trigger it, so the safe branch has to be the default.
if mode not in ("auto", "each line"): if mode not in ("auto", "each line"):
mode = "auto" mode = "auto"
if mode == "auto":
return [p.strip() for p in (paras or []) if str(p or "").strip()], ""
out, split_from = [], 0 out, split_from = [], 0
for p in paras: for p in paras:
lines = [ln for ln in (p or "").splitlines() if ln.strip()] lines = [ln for ln in (p or "").splitlines() if ln.strip()]
@@ -370,10 +361,9 @@ def expand_beats(paras, mode="auto"):
else: else:
out.append(NL.join(pending)) out.append(NL.join(pending))
note = "" note = ""
if split_from and mode == "auto": if split_from:
note = (f"{split_from} paragraph(s) held several lines and were split one beat per LINE " note = (f"beat_split '{mode}' split {split_from} multi-line paragraph(s) into "
f"-> {len(out)} beats. Separate beats with a BLANK line (or a '##' line) to control " f"{len(out)} beats")
f"this yourself")
return out, note return out, note
@@ -6154,17 +6144,12 @@ class H3LongVideos:
"EVERY ref-conditioned shot, so on a long chain it is several times " "EVERY ref-conditioned shot, so on a long chain it is several times "
"slower. Neither ever upscales a small reference."}), "slower. Neither ever upscales a small reference."}),
"beat_split": (["auto", "each line"], {"default": "auto", "beat_split": (["auto", "each line"], {"default": "auto",
"tooltip": "How the prompt box becomes beats. Beats are meant to be separated by a " "tooltip": "How the prompt box becomes beats. Beats are separated by a BLANK line "
"BLANK line (or a '##' line) -- but six beats typed on six consecutive " "(or a '##' line). auto (default): each paragraph stays one beat, so "
"lines are ONE paragraph, so they would render as one shot with six actions " "plain newlines inside a beat remain part of that beat. 'each line': every "
"crammed into it, which looks like everyone is moving at triple speed. " "content line becomes its own beat, with directive lines (wardrobe:, "
"auto (default): blank lines first, then any paragraph still holding " "seconds:, exit:, etc.) attaching to the beat they configure. Older "
"several lines is split one beat per LINE, and the info output says so. " "removed values such as 'blank line' now behave like 'auto'."}),
"'each line': every line is its own beat -- same result, stated explicitly. "
"Neither can lose a beat. (The old strict 'blank line' option was REMOVED: "
"it was the only setting that could silently collapse beats, and a stored "
"value of it now reads as 'auto'.) Directive lines (wardrobe:, seconds:, exit:) "
"are never beats -- they attach to the beat that follows them."}),
"anchor_override": ("STRING", {"multiline": True, "forceInput": True, "default": "", "anchor_override": ("STRING", {"multiline": True, "forceInput": True, "default": "",
"tooltip": "Set the persistent look explicitly instead of using the first paragraph. " "tooltip": "Set the persistent look explicitly instead of using the first paragraph. "
"When this is filled in, EVERY paragraph of the prompt box is a beat/shot -- " "When this is filled in, EVERY paragraph of the prompt box is a beat/shot -- "
+39
View File
@@ -160,6 +160,45 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
"keyframe carry", "keyframe carry",
) )
def test_expand_beats_auto_preserves_multiline_paragraph_as_one_beat(self):
beats, note = self.module.expand_beats(
["wardrobe: Maya = red jacket\nMaya enters the room.\nShe sits at the table."],
"auto",
)
self.assertEqual(
beats,
["wardrobe: Maya = red jacket\nMaya enters the room.\nShe sits at the table."],
)
self.assertEqual(note, "")
def test_expand_beats_legacy_blank_line_value_falls_back_to_auto(self):
beats, note = self.module.expand_beats(
["Maya enters the room.\nShe sits at the table."],
"blank line",
)
self.assertEqual(
beats,
["Maya enters the room.\nShe sits at the table."],
)
self.assertEqual(note, "")
def test_expand_beats_each_line_still_splits_multiline_paragraphs(self):
beats, note = self.module.expand_beats(
["wardrobe: Maya = red jacket\nMaya enters the room.\nseconds: 8\nShe sits at the table."],
"each line",
)
self.assertEqual(
beats,
[
"wardrobe: Maya = red jacket\nMaya enters the room.",
"seconds: 8\nShe sits at the table.",
],
)
self.assertIn("beat_split 'each line' split 1 multi-line paragraph(s) into 2 beats", note)
def test_timing_summary_reports_retry_and_bucket_totals(self): def test_timing_summary_reports_retry_and_bucket_totals(self):
note = self.module._format_timing_note([ note = self.module._format_timing_note([
{ {