-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathrift_engine_script.py
More file actions
141 lines (122 loc) · 5.99 KB
/
Copy pathrift_engine_script.py
File metadata and controls
141 lines (122 loc) · 5.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
"""One script, two engines.
H3 and LTX-2.5 want opposite things from a multi-shot script.
H3 chains N separate generations. Each block is one generation, and the scene
description has to be restated WORD FOR WORD in every block or the room drifts
- continuity comes from the text agreeing with itself plus the latent pin.
LTX-2.5 generates all the shots in ONE pass and finds the cuts itself, holding
character, environment, lighting and voice across them natively. Feeding it ten
verbatim repetitions of the same paragraph does not help it and buries the part
that changes.
This node takes the H3 form - the one a writer actually maintains - and emits
both. The scene block is found as the longest common prefix across the blocks,
which is exactly what "restate it verbatim" produces, and is stated once for
LTX with the per-shot remainder following as a beat list.
"""
_SEP = "\n---\n"
def _blocks(script):
parts = [b.strip() for b in script.split(_SEP)]
return [b for b in parts if b]
def _common_prefix(blocks, sentence_boundary=True):
"""Longest text shared by the start of every block.
sentence_boundary=True cuts back to the end of a sentence, which is what
the SCENE block wants - otherwise the LTX prompt opens with half a sentence
and the per-shot beats start with the other half.
It is wrong for the per-shot HOLD preamble, which by design ends mid
sentence on "... says," - cutting back to a full stop finds none and
silently returns nothing, leaving the preamble to repeat once per shot.
"""
if len(blocks) < 2:
return ""
pre = blocks[0]
for b in blocks[1:]:
n = min(len(pre), len(b))
i = 0
while i < n and pre[i] == b[i]:
i += 1
pre = pre[:i]
if not pre:
return ""
if not sentence_boundary:
return pre.strip()
cut = max(pre.rfind(". "), pre.rfind('." '), pre.rfind("? "), pre.rfind("! "))
return pre[:cut + 1].strip() if cut > 40 else ""
class RiftEngineScript:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"script": ("STRING", {
"multiline": True, "default": "",
"tooltip": "The H3 form: shots separated by --- on its own "
"line, with the scene block restated verbatim in "
"each. Write it once, here, for either engine."}),
},
"optional": {
"ltx_shot_label": ("STRING", {
"default": "Then, without moving the camera,",
"tooltip": "Joins the per-shot beats in the LTX prompt. "
"LTX-2.5 decides its own cut points, so this is "
"how you tell it whether the shots are one "
"continuous take or separate setups. For hard "
"cuts try 'Cut to:'."}),
},
}
RETURN_TYPES = ("STRING", "STRING", "INT")
RETURN_NAMES = ("h3_script", "ltx_prompt", "shot_count")
FUNCTION = "split"
CATEGORY = "H3/script"
def split(self, script, ltx_shot_label="Then, without moving the camera,"):
blocks = _blocks(script)
if not blocks:
return ("", "", 0)
scene = _common_prefix(blocks)
if scene:
tails = [b[len(scene):].strip() if b.startswith(scene) else b
for b in blocks]
# Second pass: shots 2..N usually share a "hold" preamble too -
# "She sits exactly as she was, ... and the woman says," - which is
# not a prefix of ALL blocks because shot 1 opens differently. Left
# in, it repeats once per shot and buries the beats.
hold = (_common_prefix(tails[1:], sentence_boundary=False)
if len(tails) > 2 else "")
# The shared preamble runs right up to the opening quote of the
# line - every block has one there, so the raw prefix swallows it
# and each beat loses the quote that opens its dialogue.
hold = hold.rstrip("“‘\"' \t")
if len(hold) < 25: # too short to be a real preamble
hold = ""
if hold:
tails = [tails[0]] + [
(t[len(hold):].strip() if t.startswith(hold) else t)
for t in tails[1:]]
label = ltx_shot_label.strip()
joined = tails[0]
for t in tails[1:]:
if label:
# The beat continues the joining clause, so it must not
# arrive capitalised mid-sentence - UNLESS it opens with
# dialogue, where the capital belongs to the line and
# lowercasing it corrupts what the character says.
if (t[:1].isupper() and not t[:2].isupper()
and not t.startswith(("“", '"', "‘", "'"))):
t = t[0].lower() + t[1:]
joined += " " + label + " " + t
else:
joined += " " + t
ltx = scene + (" " + hold if hold else "") + " " + joined
else:
# no verbatim scene block - the writer is not using the H3
# convention, so pass the blocks through in order rather than
# inventing structure that is not there
label = ltx_shot_label.strip()
ltx = (" " + label + " ").join(blocks) if label else " ".join(blocks)
print("[H3 Multishot] EngineScript: %d shot(s); scene block %s; "
"H3 %d chars, LTX %d chars"
% (len(blocks),
"%d chars, stated once for LTX" % len(scene) if scene
else "not detected (blocks passed through)",
len(script), len(ltx)), flush=True)
return (script, ltx, len(blocks))
NODE_CLASS_MAPPINGS = {"RiftEngineScript": RiftEngineScript}
NODE_DISPLAY_NAME_MAPPINGS = {
"RiftEngineScript": "Script for Two Engines (H3 + LTX-2.5)"}