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 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
on six consecutive lines are one paragraph, so they render as ONE shot with six
actions crammed into it -- which reads as characters moving at triple speed, not
as a splitting problem.
it is also easy to misread once a dedicated beat editor exists: inside the
beat editor, one textbox is one beat, so plain newlines inside that textbox
should stay inside the same beat rather than quietly creating new shots.
mode:
'auto' -- blank lines first; any paragraph still holding more than one
content line is then split one beat per line, and says so.
'each line' -- every content line is its own beat. Same result as 'auto'; kept
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.
'auto' -- preserve each paragraph as one beat.
'each line' -- every content line is its own beat, with directive lines
attaching to the beat they configure.
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
trail the paragraph."""
# Any unrecognized mode means AUTO, never "do nothing". An earlier version fell
# through an if/elif with no else and silently DROPPED every multi-line paragraph
# -- six beats arrived as two shots with four beats simply gone. A stale value on
# 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.
# Any unrecognized mode means AUTO, never "do nothing". Older workflows may
# still store removed values such as 'blank line'; those should preserve whole
# paragraphs instead of unexpectedly splitting every newline into a beat.
if mode not in ("auto", "each line"):
mode = "auto"
if mode == "auto":
return [p.strip() for p in (paras or []) if str(p or "").strip()], ""
out, split_from = [], 0
for p in paras:
lines = [ln for ln in (p or "").splitlines() if ln.strip()]
@@ -370,10 +361,9 @@ def expand_beats(paras, mode="auto"):
else:
out.append(NL.join(pending))
note = ""
if split_from and mode == "auto":
note = (f"{split_from} paragraph(s) held several lines and were split one beat per LINE "
f"-> {len(out)} beats. Separate beats with a BLANK line (or a '##' line) to control "
f"this yourself")
if split_from:
note = (f"beat_split '{mode}' split {split_from} multi-line paragraph(s) into "
f"{len(out)} beats")
return out, note
@@ -6154,17 +6144,12 @@ class H3LongVideos:
"EVERY ref-conditioned shot, so on a long chain it is several times "
"slower. Neither ever upscales a small reference."}),
"beat_split": (["auto", "each line"], {"default": "auto",
"tooltip": "How the prompt box becomes beats. Beats are meant to be separated by a "
"BLANK line (or a '##' line) -- but six beats typed on six consecutive "
"lines are ONE paragraph, so they would render as one shot with six actions "
"crammed into it, which looks like everyone is moving at triple speed. "
"auto (default): blank lines first, then any paragraph still holding "
"several lines is split one beat per LINE, and the info output says so. "
"'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."}),
"tooltip": "How the prompt box becomes beats. Beats are separated by a BLANK line "
"(or a '##' line). auto (default): each paragraph stays one beat, so "
"plain newlines inside a beat remain part of that beat. 'each line': every "
"content line becomes its own beat, with directive lines (wardrobe:, "
"seconds:, exit:, etc.) attaching to the beat they configure. Older "
"removed values such as 'blank line' now behave like 'auto'."}),
"anchor_override": ("STRING", {"multiline": True, "forceInput": True, "default": "",
"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 -- "
+39
View File
@@ -160,6 +160,45 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
"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):
note = self.module._format_timing_note([
{