Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9923a3e417 | ||
|
|
ea6fae7e56 |
@@ -38,6 +38,7 @@
|
|||||||
- The local Dumas prompt-engineering fork has been removed from this node. Long Videos now wraps the upstream sampler/engine directly so it can track the source project again.
|
- The local Dumas prompt-engineering fork has been removed from this node. Long Videos now wraps the upstream sampler/engine directly so it can track the source project again.
|
||||||
- Upstream compatibility keys `H3LongVideos`, `H3LongVideosFL2VA`, `H3LongVideosV1`, and `H3LongVideosREF2VA` are also registered to the same class.
|
- Upstream compatibility keys `H3LongVideos`, `H3LongVideosFL2VA`, `H3LongVideosV1`, and `H3LongVideosREF2VA` are also registered to the same class.
|
||||||
- The old Dumas browser widget grouping script is disabled for this node because it targeted controls that no longer exist on the upstream sampler.
|
- The old Dumas browser widget grouping script is disabled for this node because it targeted controls that no longer exist on the upstream sampler.
|
||||||
|
- `handoff_frames` extends the upstream last-frame handoff: `1` keeps the current single keyframe behavior; higher values keep that final-frame keyframe and add earlier tail frames from the previous shot as claimed reference context for the next beat.
|
||||||
- Upstream license text is included in [`H3_LONGVIDEOS_UPSTREAM_LICENSE.txt`](./H3_LONGVIDEOS_UPSTREAM_LICENSE.txt).
|
- Upstream license text is included in [`H3_LONGVIDEOS_UPSTREAM_LICENSE.txt`](./H3_LONGVIDEOS_UPSTREAM_LICENSE.txt).
|
||||||
|
|
||||||
- `Dumas H3 Latent Upscale Params`
|
- `Dumas H3 Latent Upscale Params`
|
||||||
@@ -48,7 +49,9 @@
|
|||||||
- `Dumas H3 Beat Prompt`
|
- `Dumas H3 Beat Prompt`
|
||||||
- Inputs: authored through the custom front-end beat editor
|
- Inputs: authored through the custom front-end beat editor
|
||||||
- Output: `prompt`
|
- Output: `prompt`
|
||||||
- Builds one H3 prompt block per beat, with quick controls for per-shot timing, continuity, ref behavior, anchor additions, soundscape, and music while staying compatible with direct text editing.
|
- Builds an upstream-compatible Long Videos prompt: optional scene paragraph, optional character sheet, then one blank-line-separated textbox per beat.
|
||||||
|
- Per-beat helpers only emit upstream-supported state directives: `remove:` / `removed:` / `off:` and `add:` / `wear:` / `wearing:`.
|
||||||
|
- Old Dumas-only beat directives such as `seconds:`, `continuity:`, `ref_mode:`, `ref_noise_aug:`, `anchor_add:`, `soundscape:`, and `music:` are stripped from the generated prompt so they are not sent to the upstream node as visible text.
|
||||||
|
|
||||||
- `Dumas H3 Prompt Curator`
|
- `Dumas H3 Prompt Curator`
|
||||||
- Inputs: `action_prompt`, `anatomy_guard`, `subject_count_guard`, optional `anchor`, optional `soundscape`, optional `bgm`, optional `ref_1` through `ref_9`
|
- Inputs: `action_prompt`, `anatomy_guard`, `subject_count_guard`, optional `anchor`, optional `soundscape`, optional `bgm`, optional `ref_1` through `ref_9`
|
||||||
|
|||||||
+55
-8
@@ -2,11 +2,46 @@ import json
|
|||||||
|
|
||||||
|
|
||||||
_DEFAULT_BEAT = "Describe this beat."
|
_DEFAULT_BEAT = "Describe this beat."
|
||||||
_DEFAULT_STATE = {"beats": [{"text": _DEFAULT_BEAT}]}
|
_DEFAULT_STATE = {"scene": "", "character_sheet": "", "beats": [{"text": _DEFAULT_BEAT}]}
|
||||||
|
_LEGACY_DIRECTIVE_PREFIXES = (
|
||||||
|
"seconds",
|
||||||
|
"duration",
|
||||||
|
"continuity",
|
||||||
|
"ref_mode",
|
||||||
|
"ref_noise_aug",
|
||||||
|
"anchor_add",
|
||||||
|
"overall_soundscape",
|
||||||
|
"soundscape",
|
||||||
|
"non_diegetic_music",
|
||||||
|
"music",
|
||||||
|
"wardrobe",
|
||||||
|
"enter",
|
||||||
|
"exit",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _clone_default_state():
|
def _clone_default_state():
|
||||||
return {"beats": [{"text": _DEFAULT_BEAT}]}
|
return {
|
||||||
|
"scene": "",
|
||||||
|
"character_sheet": "",
|
||||||
|
"beats": [{"text": _DEFAULT_BEAT}],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_legacy_directives(text):
|
||||||
|
"""Remove directives from the abandoned Dumas Long Videos fork.
|
||||||
|
|
||||||
|
The upstream Long Videos node sends unknown field labels to the model as text,
|
||||||
|
so this builder strips the old managed controls rather than emitting prompts
|
||||||
|
that ask H3 to draw labels such as "seconds:" or "music:" in the frame.
|
||||||
|
"""
|
||||||
|
kept = []
|
||||||
|
for line in str(text or "").splitlines():
|
||||||
|
lowered = line.strip().lower()
|
||||||
|
if any(lowered.startswith(f"{name}:") for name in _LEGACY_DIRECTIVE_PREFIXES):
|
||||||
|
continue
|
||||||
|
kept.append(line)
|
||||||
|
return "\n".join(kept).strip()
|
||||||
|
|
||||||
|
|
||||||
def _parse_beat_prompt_state(value):
|
def _parse_beat_prompt_state(value):
|
||||||
@@ -21,6 +56,8 @@ def _parse_beat_prompt_state(value):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return _clone_default_state()
|
return _clone_default_state()
|
||||||
|
|
||||||
|
scene = str(raw.get("scene") or "")
|
||||||
|
character_sheet = str(raw.get("character_sheet") or "")
|
||||||
beats = []
|
beats = []
|
||||||
for item in list(raw.get("beats") or []):
|
for item in list(raw.get("beats") or []):
|
||||||
if isinstance(item, dict):
|
if isinstance(item, dict):
|
||||||
@@ -30,15 +67,25 @@ def _parse_beat_prompt_state(value):
|
|||||||
beats.append({"text": text})
|
beats.append({"text": text})
|
||||||
|
|
||||||
if not beats:
|
if not beats:
|
||||||
return _clone_default_state()
|
beats = [{"text": _DEFAULT_BEAT}]
|
||||||
return {"beats": beats}
|
return {
|
||||||
|
"scene": scene,
|
||||||
|
"character_sheet": character_sheet,
|
||||||
|
"beats": beats,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _assemble_beat_prompt(state):
|
def _assemble_beat_prompt(state):
|
||||||
parsed = _parse_beat_prompt_state(state)
|
parsed = _parse_beat_prompt_state(state)
|
||||||
chunks = []
|
chunks = []
|
||||||
|
scene = str(parsed.get("scene") or "").strip()
|
||||||
|
if scene:
|
||||||
|
chunks.append(scene)
|
||||||
|
character_sheet = str(parsed.get("character_sheet") or "").strip()
|
||||||
|
if character_sheet:
|
||||||
|
chunks.append(character_sheet)
|
||||||
for beat in parsed["beats"]:
|
for beat in parsed["beats"]:
|
||||||
text = str(beat.get("text") or "").strip()
|
text = _strip_legacy_directives(beat.get("text") or "")
|
||||||
if text:
|
if text:
|
||||||
chunks.append(text)
|
chunks.append(text)
|
||||||
return "\n\n".join(chunks)
|
return "\n\n".join(chunks)
|
||||||
@@ -46,9 +93,9 @@ def _assemble_beat_prompt(state):
|
|||||||
|
|
||||||
class DumasH3BeatPromptNode:
|
class DumasH3BeatPromptNode:
|
||||||
DESCRIPTION = (
|
DESCRIPTION = (
|
||||||
"Build a MiniMax H3 prompt from one textbox per beat, with a front-end beat "
|
"Build an upstream MiniMax H3 Long Videos prompt: optional scene paragraph, "
|
||||||
"editor that can append directive examples and expose per-shot controls for "
|
"optional character sheet, then one blank-line-separated textbox per beat. "
|
||||||
"timing, continuity, ref behavior, anchor additions, soundscape, and music."
|
"Per-beat helpers only emit directives the upstream node understands."
|
||||||
)
|
)
|
||||||
RETURN_TYPES = ("STRING",)
|
RETURN_TYPES = ("STRING",)
|
||||||
RETURN_NAMES = ("prompt",)
|
RETURN_NAMES = ("prompt",)
|
||||||
|
|||||||
@@ -5831,6 +5831,24 @@ def handoff_claim(n):
|
|||||||
f"anybody new.")
|
f"anybody new.")
|
||||||
|
|
||||||
|
|
||||||
|
def handoff_context_claim(first, last):
|
||||||
|
"""Claim previous-shot tail frames carried as reference context.
|
||||||
|
|
||||||
|
These are not keyframes. They are context stills that show the room, camera
|
||||||
|
path, and motion immediately before the hard handoff keyframe. They must be
|
||||||
|
named in the prompt because every reference image the VLM sees needs a job:
|
||||||
|
unnamed pictures are free to become extra subjects.
|
||||||
|
"""
|
||||||
|
first, last = int(first), int(last)
|
||||||
|
if last <= first:
|
||||||
|
return (f" <Picture {first}> is a continuity context frame from immediately "
|
||||||
|
f"before this shot: same place, same camera path, same people, no "
|
||||||
|
f"new subject.")
|
||||||
|
return (f" <Picture {first}> through <Picture {last}> are continuity context "
|
||||||
|
f"frames from immediately before this shot: same place, same camera "
|
||||||
|
f"path, same people, no new subjects.")
|
||||||
|
|
||||||
|
|
||||||
def room_claim(n, present, joining):
|
def room_claim(n, present, joining):
|
||||||
"""Claim a handoff carried as a reference because somebody NEW is in the shot.
|
"""Claim a handoff carried as a reference because somebody NEW is in the shot.
|
||||||
|
|
||||||
@@ -7062,6 +7080,7 @@ _WIDGET_RANGE = {
|
|||||||
"pace": (1.0, 0.25, 2.0, float),
|
"pace": (1.0, 0.25, 2.0, float),
|
||||||
"ambient_level": (0.25, 0.0, 1.0, float),
|
"ambient_level": (0.25, 0.0, 1.0, float),
|
||||||
"foley_level": (0.35, 0.0, 1.0, float),
|
"foley_level": (0.35, 0.0, 1.0, float),
|
||||||
|
"handoff_frames": (1, 1, MAX_FRAMES, int),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -7582,6 +7601,18 @@ class H3LongVideos:
|
|||||||
"It is synthesis, not a recording: a click, a rattle, "
|
"It is synthesis, not a recording: a click, a rattle, "
|
||||||
"a rustle, in the right place. Nothing vocal is ever "
|
"a rustle, in the right place. Nothing vocal is ever "
|
||||||
"built. 0 turns it off; needs auto_sound on."}),
|
"built. 0 turns it off; needs auto_sound on."}),
|
||||||
|
# APPENDED. Saved workflows restore widget values by position.
|
||||||
|
"handoff_frames": ("INT", {"default": 1, "min": 1, "max": MAX_FRAMES,
|
||||||
|
"tooltip": "How many frames from the previous shot are used to "
|
||||||
|
"condition the next one. 1 is the upstream default: "
|
||||||
|
"the previous final frame becomes the next shot's "
|
||||||
|
"keyframe. Values above 1 keep that final-frame "
|
||||||
|
"keyframe and append the earlier tail frames as "
|
||||||
|
"claimed reference context, so the next beat can see "
|
||||||
|
"more of the incoming motion and room continuity. "
|
||||||
|
"The extra frames are references, not extra "
|
||||||
|
"keyframes, so seam trimming still removes only the "
|
||||||
|
"duplicated opening frame."}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7608,6 +7639,7 @@ class H3LongVideos:
|
|||||||
character_guard=True, pace=1.0, auto_sound=True, hold_scene_state=True,
|
character_guard=True, pace=1.0, auto_sound=True, hold_scene_state=True,
|
||||||
mouths_shut_when_no_line=True, hold_gaze=True,
|
mouths_shut_when_no_line=True, hold_gaze=True,
|
||||||
ambient_audio=None, ambient_level=0.25, foley_level=0.35,
|
ambient_audio=None, ambient_level=0.25, foley_level=0.35,
|
||||||
|
handoff_frames=1,
|
||||||
**_removed):
|
**_removed):
|
||||||
# **_removed: a workflow saved with the old `save_defaults` widget still sends
|
# **_removed: a workflow saved with the old `save_defaults` widget still sends
|
||||||
# it. Swallowed rather than raising, so an existing workflow keeps loading.
|
# it. Swallowed rather than raising, so an existing workflow keeps loading.
|
||||||
@@ -7632,7 +7664,8 @@ class H3LongVideos:
|
|||||||
shift_video=shift_video, shift_audio=shift_audio,
|
shift_video=shift_video, shift_audio=shift_audio,
|
||||||
ref_noise_aug=ref_noise_aug, latent_upscale_scale=latent_upscale_scale,
|
ref_noise_aug=ref_noise_aug, latent_upscale_scale=latent_upscale_scale,
|
||||||
upscale_target_short_edge=upscale_target_short_edge,
|
upscale_target_short_edge=upscale_target_short_edge,
|
||||||
upscale_batch=upscale_batch, pace=pace))
|
upscale_batch=upscale_batch, pace=pace,
|
||||||
|
handoff_frames=handoff_frames))
|
||||||
megapixels, shot_seconds = _fixed["megapixels"], _fixed["shot_seconds"]
|
megapixels, shot_seconds = _fixed["megapixels"], _fixed["shot_seconds"]
|
||||||
steps, cfg = _fixed["steps"], _fixed["cfg"]
|
steps, cfg = _fixed["steps"], _fixed["cfg"]
|
||||||
shift_video, shift_audio = _fixed["shift_video"], _fixed["shift_audio"]
|
shift_video, shift_audio = _fixed["shift_video"], _fixed["shift_audio"]
|
||||||
@@ -7640,6 +7673,7 @@ class H3LongVideos:
|
|||||||
latent_upscale_scale = _fixed["latent_upscale_scale"]
|
latent_upscale_scale = _fixed["latent_upscale_scale"]
|
||||||
upscale_target_short_edge = _fixed["upscale_target_short_edge"]
|
upscale_target_short_edge = _fixed["upscale_target_short_edge"]
|
||||||
upscale_batch, pace = _fixed["upscale_batch"], _fixed["pace"]
|
upscale_batch, pace = _fixed["upscale_batch"], _fixed["pace"]
|
||||||
|
handoff_frames = _fixed["handoff_frames"]
|
||||||
notes.extend(_fixnotes)
|
notes.extend(_fixnotes)
|
||||||
# <Picture N> means ref_image_N, the socket. Everything downstream works on
|
# <Picture N> means ref_image_N, the socket. Everything downstream works on
|
||||||
# the packed roster instead, so translate once, here, before anything has
|
# the packed roster instead, so translate once, here, before anything has
|
||||||
@@ -10234,6 +10268,7 @@ class H3LongVideos:
|
|||||||
negative = clip.encode_from_tokens_scheduled(clip.tokenize(""))
|
negative = clip.encode_from_tokens_scheduled(clip.tokenize(""))
|
||||||
|
|
||||||
handoff = first_frame
|
handoff = first_frame
|
||||||
|
handoff_context = None
|
||||||
# Where the time actually goes. Sampling and decode trade off against each
|
# Where the time actually goes. Sampling and decode trade off against each
|
||||||
# other -- latent_upscale buys cheaper sampling and pays for it at decode,
|
# other -- latent_upscale buys cheaper sampling and pays for it at decode,
|
||||||
# and which side wins depends on `steps`. Reported so the trade is a
|
# and which side wins depends on `steps`. Reported so the trade is a
|
||||||
@@ -10365,6 +10400,20 @@ class H3LongVideos:
|
|||||||
# here rather than inside build_conditioning because the claim is text,
|
# here rather than inside build_conditioning because the claim is text,
|
||||||
# and the text is assembled up here.
|
# and the text is assembled up here.
|
||||||
_shot_refs = list(shot_refs_all[i]) + _extra
|
_shot_refs = list(shot_refs_all[i]) + _extra
|
||||||
|
_ctx_refs = []
|
||||||
|
_keyframe_ok = ref_noise_aug is None or float(ref_noise_aug) >= KEYFRAME_SAFE_AUG
|
||||||
|
if (handoff_frames > 1 and shot_handoff is not None and handoff_context is not None
|
||||||
|
and not _handoff_ref and _keyframe_ok):
|
||||||
|
try:
|
||||||
|
_ctx_refs = [handoff_context[j:j + 1]
|
||||||
|
for j in range(int(handoff_context.shape[0]))]
|
||||||
|
except Exception:
|
||||||
|
_ctx_refs = []
|
||||||
|
if _ctx_refs:
|
||||||
|
_first = len(_shot_refs) + 1
|
||||||
|
_last = _first + len(_ctx_refs) - 1
|
||||||
|
shot_prompt = shot_prompt + handoff_context_claim(_first, _last)
|
||||||
|
_shot_refs.extend(_ctx_refs)
|
||||||
if _handoff_ref:
|
if _handoff_ref:
|
||||||
# Carried for the ROOM, with somebody new in the shot -- so the
|
# Carried for the ROOM, with somebody new in the shot -- so the
|
||||||
# standing claim is exactly wrong here ("joined by anybody new") and
|
# standing claim is exactly wrong here ("joined by anybody new") and
|
||||||
@@ -10380,7 +10429,7 @@ class H3LongVideos:
|
|||||||
sent_text[i] = shot_prompt
|
sent_text[i] = shot_prompt
|
||||||
cond, latent, fc, demoted = build_conditioning(
|
cond, latent, fc, demoted = build_conditioning(
|
||||||
clip, vae, audio_vae, shot_prompt, w, h, lens[i],
|
clip, vae, audio_vae, shot_prompt, w, h, lens[i],
|
||||||
handoff=shot_handoff, refs=list(shot_refs_all[i]) + _extra,
|
handoff=shot_handoff, refs=_shot_refs,
|
||||||
ref_noise_aug=ref_noise_aug, silent=silent,
|
ref_noise_aug=ref_noise_aug, silent=silent,
|
||||||
handoff_as_ref=_handoff_ref)
|
handoff_as_ref=_handoff_ref)
|
||||||
if demoted and not _aug_warned:
|
if demoted and not _aug_warned:
|
||||||
@@ -10457,6 +10506,16 @@ class H3LongVideos:
|
|||||||
# 0..1, and feeding that back in to be re-encoded every boundary is a
|
# 0..1, and feeding that back in to be re-encoded every boundary is a
|
||||||
# drift that accumulates rather than cancels.
|
# drift that accumulates rather than cancels.
|
||||||
handoff = hand_src[-1:].detach().clamp(0.0, 1.0).to("cpu", copy=True)
|
handoff = hand_src[-1:].detach().clamp(0.0, 1.0).to("cpu", copy=True)
|
||||||
|
handoff_context = None
|
||||||
|
if handoff_frames > 1:
|
||||||
|
try:
|
||||||
|
available = max(0, int(hand_src.shape[0]) - 1)
|
||||||
|
want = min(max(0, int(handoff_frames) - 1), available)
|
||||||
|
if want > 0:
|
||||||
|
handoff_context = hand_src[-(want + 1):-1].detach().clamp(
|
||||||
|
0.0, 1.0).to("cpu", copy=True)
|
||||||
|
except Exception:
|
||||||
|
handoff_context = None
|
||||||
# Keep a frame for the shot they come back on -- but ONLY from a shot that
|
# Keep a frame for the shot they come back on -- but ONLY from a shot that
|
||||||
# was theirs alone.
|
# was theirs alone.
|
||||||
#
|
#
|
||||||
|
|||||||
+74
-100
@@ -8,32 +8,15 @@ const DEFAULT_W = 520;
|
|||||||
const DEFAULT_H = 340;
|
const DEFAULT_H = 340;
|
||||||
const DEFAULT_BEAT = "Describe this beat.";
|
const DEFAULT_BEAT = "Describe this beat.";
|
||||||
const STATE_PROPERTY = "dumas_h3_beat_prompt_state";
|
const STATE_PROPERTY = "dumas_h3_beat_prompt_state";
|
||||||
const CONTINUITY_OPTIONS = ["", "soft carry", "hard cut", "keyframe carry", "handoff ref"];
|
|
||||||
const REF_MODE_OPTIONS = ["", "auto ref2v", "where tagged", "first shot", "every shot", "every shot + handoff ref"];
|
|
||||||
const MANAGED_DIRECTIVES = {
|
const MANAGED_DIRECTIVES = {
|
||||||
seconds: ["seconds", "duration"],
|
remove: ["remove", "removed", "off"],
|
||||||
continuity: ["continuity"],
|
add: ["add", "wear", "wearing"],
|
||||||
ref_mode: ["ref_mode"],
|
|
||||||
ref_noise_aug: ["ref_noise_aug"],
|
|
||||||
anchor_add: ["anchor_add"],
|
|
||||||
overall_soundscape: ["overall_soundscape", "soundscape"],
|
|
||||||
non_diegetic_music: ["non_diegetic_music", "music"],
|
|
||||||
};
|
};
|
||||||
const DIRECTIVE_EXAMPLES = [
|
const DIRECTIVE_EXAMPLES = [
|
||||||
["wardrobe set", "wardrobe: Maya = grey shorts, red jacket"],
|
["remove", "remove: red jacket"],
|
||||||
["wardrobe add", "wardrobe: Maya += red jacket"],
|
["off", "off: steel collar"],
|
||||||
["wardrobe remove", "wardrobe: Maya -= red jacket"],
|
["add", "add: white shirt underneath"],
|
||||||
["seconds", "seconds: 8"],
|
["wearing", "wearing: black coat"],
|
||||||
["exit", "exit: Maya"],
|
|
||||||
["enter", "enter: Jon"],
|
|
||||||
["continuity", "continuity: hard cut"],
|
|
||||||
["ref_mode", "ref_mode: every shot"],
|
|
||||||
["ref_noise_aug", "ref_noise_aug: 0.92"],
|
|
||||||
["anchor_add", "anchor_add: harsh sodium-vapor spill, wet pavement, long-lens compression"],
|
|
||||||
["overall_soundscape", "overall_soundscape: soft rain, distant traffic"],
|
|
||||||
["non_diegetic_music", "non_diegetic_music: tense analog synth pulse"],
|
|
||||||
["soundscape", "soundscape: fluorescent room tone, faint HVAC hum"],
|
|
||||||
["music", "music: low ominous cello and sparse percussion"],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function injectCSS() {
|
function injectCSS() {
|
||||||
@@ -183,7 +166,7 @@ function injectCSS() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function defaultState() {
|
function defaultState() {
|
||||||
return { beats: [{ text: DEFAULT_BEAT }] };
|
return { scene: "", character_sheet: "", beats: [{ text: DEFAULT_BEAT }] };
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeState(value) {
|
function normalizeState(value) {
|
||||||
@@ -200,7 +183,11 @@ function normalizeState(value) {
|
|||||||
const normalized = beats.map((beat) => ({
|
const normalized = beats.map((beat) => ({
|
||||||
text: typeof beat?.text === "string" ? beat.text : String(beat?.text || ""),
|
text: typeof beat?.text === "string" ? beat.text : String(beat?.text || ""),
|
||||||
}));
|
}));
|
||||||
return normalized.length ? { beats: normalized } : defaultState();
|
return {
|
||||||
|
scene: typeof parsed.scene === "string" ? parsed.scene : String(parsed.scene || ""),
|
||||||
|
character_sheet: typeof parsed.character_sheet === "string" ? parsed.character_sheet : String(parsed.character_sheet || ""),
|
||||||
|
beats: normalized.length ? normalized : [{ text: DEFAULT_BEAT }],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function readState(node) {
|
function readState(node) {
|
||||||
@@ -321,6 +308,51 @@ function renderUI(node) {
|
|||||||
node._dh3bpRenderedState = JSON.stringify(state);
|
node._dh3bpRenderedState = JSON.stringify(state);
|
||||||
ui.list.innerHTML = "";
|
ui.list.innerHTML = "";
|
||||||
|
|
||||||
|
const buildTopTextarea = ({ labelText, placeholder, value, onInput }) => {
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "dh3bp-beat";
|
||||||
|
|
||||||
|
const label = document.createElement("div");
|
||||||
|
label.className = "dh3bp-label";
|
||||||
|
label.textContent = labelText;
|
||||||
|
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.className = "dh3bp-text";
|
||||||
|
textarea.placeholder = placeholder;
|
||||||
|
textarea.value = value || "";
|
||||||
|
textarea.addEventListener("input", () => {
|
||||||
|
onInput(textarea.value);
|
||||||
|
updateTextareaHeight(textarea);
|
||||||
|
});
|
||||||
|
textarea.addEventListener("keydown", stopCanvasKeyboard);
|
||||||
|
|
||||||
|
card.append(label, textarea);
|
||||||
|
updateTextareaHeight(textarea);
|
||||||
|
return card;
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.list.appendChild(buildTopTextarea({
|
||||||
|
labelText: "Scene paragraph",
|
||||||
|
placeholder: "Optional. Persistent location, lighting, camera, tone. Leave empty if you wire the Long Videos anchor input.",
|
||||||
|
value: state.scene,
|
||||||
|
onInput: (value) => {
|
||||||
|
const next = readState(node);
|
||||||
|
next.scene = value;
|
||||||
|
writeState(node, next);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
ui.list.appendChild(buildTopTextarea({
|
||||||
|
labelText: "Character sheet",
|
||||||
|
placeholder: "Optional. One character per line, e.g. Maya: 27, she, silver hair, red jacket, the woman in <Picture 1>.",
|
||||||
|
value: state.character_sheet,
|
||||||
|
onInput: (value) => {
|
||||||
|
const next = readState(node);
|
||||||
|
next.character_sheet = value;
|
||||||
|
writeState(node, next);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
state.beats.forEach((beat, index) => {
|
state.beats.forEach((beat, index) => {
|
||||||
const card = document.createElement("div");
|
const card = document.createElement("div");
|
||||||
card.className = "dh3bp-beat";
|
card.className = "dh3bp-beat";
|
||||||
@@ -379,85 +411,27 @@ function renderUI(node) {
|
|||||||
return wrap;
|
return wrap;
|
||||||
};
|
};
|
||||||
|
|
||||||
const secondsInput = document.createElement("input");
|
const removeInput = document.createElement("input");
|
||||||
secondsInput.className = "dh3bp-input";
|
removeInput.className = "dh3bp-input";
|
||||||
secondsInput.type = "text";
|
removeInput.type = "text";
|
||||||
secondsInput.placeholder = "8";
|
removeInput.placeholder = "red jacket";
|
||||||
secondsInput.value = readDirectiveValue(beat.text, MANAGED_DIRECTIVES.seconds);
|
removeInput.value = readDirectiveValue(beat.text, MANAGED_DIRECTIVES.remove);
|
||||||
secondsInput.addEventListener("input", () => {
|
removeInput.addEventListener("input", () => {
|
||||||
applyTextUpdate(setDirectiveValue(textarea.value, "seconds", MANAGED_DIRECTIVES.seconds, secondsInput.value));
|
applyTextUpdate(setDirectiveValue(textarea.value, "remove", MANAGED_DIRECTIVES.remove, removeInput.value));
|
||||||
});
|
});
|
||||||
|
|
||||||
const continuitySelect = document.createElement("select");
|
const addInput = document.createElement("input");
|
||||||
continuitySelect.className = "dh3bp-select";
|
addInput.className = "dh3bp-input";
|
||||||
CONTINUITY_OPTIONS.forEach((value) => {
|
addInput.type = "text";
|
||||||
const option = document.createElement("option");
|
addInput.placeholder = "white shirt underneath";
|
||||||
option.value = value;
|
addInput.value = readDirectiveValue(beat.text, MANAGED_DIRECTIVES.add);
|
||||||
option.textContent = value || "Default";
|
addInput.addEventListener("input", () => {
|
||||||
continuitySelect.appendChild(option);
|
applyTextUpdate(setDirectiveValue(textarea.value, "add", MANAGED_DIRECTIVES.add, addInput.value));
|
||||||
});
|
|
||||||
continuitySelect.value = readDirectiveValue(beat.text, MANAGED_DIRECTIVES.continuity);
|
|
||||||
continuitySelect.addEventListener("change", () => {
|
|
||||||
applyTextUpdate(setDirectiveValue(textarea.value, "continuity", MANAGED_DIRECTIVES.continuity, continuitySelect.value));
|
|
||||||
});
|
|
||||||
|
|
||||||
const refModeSelect = document.createElement("select");
|
|
||||||
refModeSelect.className = "dh3bp-select";
|
|
||||||
REF_MODE_OPTIONS.forEach((value) => {
|
|
||||||
const option = document.createElement("option");
|
|
||||||
option.value = value;
|
|
||||||
option.textContent = value || "Global";
|
|
||||||
refModeSelect.appendChild(option);
|
|
||||||
});
|
|
||||||
refModeSelect.value = readDirectiveValue(beat.text, MANAGED_DIRECTIVES.ref_mode);
|
|
||||||
refModeSelect.addEventListener("change", () => {
|
|
||||||
applyTextUpdate(setDirectiveValue(textarea.value, "ref_mode", MANAGED_DIRECTIVES.ref_mode, refModeSelect.value));
|
|
||||||
});
|
|
||||||
|
|
||||||
const refNoiseInput = document.createElement("input");
|
|
||||||
refNoiseInput.className = "dh3bp-input";
|
|
||||||
refNoiseInput.type = "text";
|
|
||||||
refNoiseInput.placeholder = "0.95";
|
|
||||||
refNoiseInput.value = readDirectiveValue(beat.text, MANAGED_DIRECTIVES.ref_noise_aug);
|
|
||||||
refNoiseInput.addEventListener("input", () => {
|
|
||||||
applyTextUpdate(setDirectiveValue(textarea.value, "ref_noise_aug", MANAGED_DIRECTIVES.ref_noise_aug, refNoiseInput.value));
|
|
||||||
});
|
|
||||||
|
|
||||||
const anchorInput = document.createElement("input");
|
|
||||||
anchorInput.className = "dh3bp-input";
|
|
||||||
anchorInput.type = "text";
|
|
||||||
anchorInput.placeholder = "extra per-shot style treatment";
|
|
||||||
anchorInput.value = readDirectiveValue(beat.text, MANAGED_DIRECTIVES.anchor_add);
|
|
||||||
anchorInput.addEventListener("input", () => {
|
|
||||||
applyTextUpdate(setDirectiveValue(textarea.value, "anchor_add", MANAGED_DIRECTIVES.anchor_add, anchorInput.value));
|
|
||||||
});
|
|
||||||
|
|
||||||
const soundscapeInput = document.createElement("input");
|
|
||||||
soundscapeInput.className = "dh3bp-input";
|
|
||||||
soundscapeInput.type = "text";
|
|
||||||
soundscapeInput.placeholder = "faint traffic, loose sign rattle";
|
|
||||||
soundscapeInput.value = readDirectiveValue(beat.text, MANAGED_DIRECTIVES.overall_soundscape);
|
|
||||||
soundscapeInput.addEventListener("input", () => {
|
|
||||||
applyTextUpdate(setDirectiveValue(textarea.value, "overall_soundscape", MANAGED_DIRECTIVES.overall_soundscape, soundscapeInput.value));
|
|
||||||
});
|
|
||||||
|
|
||||||
const musicInput = document.createElement("input");
|
|
||||||
musicInput.className = "dh3bp-input";
|
|
||||||
musicInput.type = "text";
|
|
||||||
musicInput.placeholder = "low pulsing synth tension";
|
|
||||||
musicInput.value = readDirectiveValue(beat.text, MANAGED_DIRECTIVES.non_diegetic_music);
|
|
||||||
musicInput.addEventListener("input", () => {
|
|
||||||
applyTextUpdate(setDirectiveValue(textarea.value, "non_diegetic_music", MANAGED_DIRECTIVES.non_diegetic_music, musicInput.value));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
controls.append(
|
controls.append(
|
||||||
buildField({ labelText: "Seconds", input: secondsInput }),
|
buildField({ labelText: "Remove from memory", input: removeInput }),
|
||||||
buildField({ labelText: "Continuity", input: continuitySelect }),
|
buildField({ labelText: "Add to memory", input: addInput }),
|
||||||
buildField({ labelText: "Ref Mode", input: refModeSelect }),
|
|
||||||
buildField({ labelText: "Ref Noise Aug", input: refNoiseInput }),
|
|
||||||
buildField({ labelText: "Anchor Add", className: "dh3bp-control-wide", input: anchorInput }),
|
|
||||||
buildField({ labelText: "Shot Soundscape", className: "dh3bp-control-wide", input: soundscapeInput }),
|
|
||||||
buildField({ labelText: "Shot Music", className: "dh3bp-control-wide", input: musicInput }),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const directives = document.createElement("div");
|
const directives = document.createElement("div");
|
||||||
@@ -501,7 +475,7 @@ function setupNode(node) {
|
|||||||
title.textContent = "Beat Prompt Builder";
|
title.textContent = "Beat Prompt Builder";
|
||||||
const subtitle = document.createElement("div");
|
const subtitle = document.createElement("div");
|
||||||
subtitle.className = "dh3bp-subtitle";
|
subtitle.className = "dh3bp-subtitle";
|
||||||
subtitle.textContent = "One textbox per H3 beat, plus per-shot controls for timing, ref behavior, continuity, anchor adds, and audio directives.";
|
subtitle.textContent = "Upstream Long Videos format: optional scene, optional character sheet, then one blank-line-separated beat per shot.";
|
||||||
titleWrap.append(title, subtitle);
|
titleWrap.append(title, subtitle);
|
||||||
|
|
||||||
const addButton = document.createElement("button");
|
const addButton = document.createElement("button");
|
||||||
|
|||||||
@@ -11,35 +11,62 @@ class DumasH3BeatPromptTests(unittest.TestCase):
|
|||||||
state = self.module._parse_beat_prompt_state("not json")
|
state = self.module._parse_beat_prompt_state("not json")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
state,
|
state,
|
||||||
{"beats": [{"text": "Describe this beat."}]},
|
{
|
||||||
|
"scene": "",
|
||||||
|
"character_sheet": "",
|
||||||
|
"beats": [{"text": "Describe this beat."}],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_assemble_prompt_joins_beats_with_blank_lines(self):
|
def test_assemble_prompt_outputs_upstream_sections(self):
|
||||||
prompt = self.module._assemble_beat_prompt(
|
prompt = self.module._assemble_beat_prompt(
|
||||||
{
|
{
|
||||||
|
"scene": "A rainy kitchen at night.",
|
||||||
|
"character_sheet": "Maya: 27, she, red jacket, silver hair.",
|
||||||
"beats": [
|
"beats": [
|
||||||
{"text": "A woman enters the room."},
|
{"text": "Maya enters the room."},
|
||||||
{"text": "wardrobe: Maya = red jacket\nShe sits at the table."},
|
{"text": "remove: red jacket\nadd: white shirt underneath\nShe sits at the table."},
|
||||||
{"text": " "},
|
{"text": " "},
|
||||||
{"text": "music: low synth pulse"},
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
prompt,
|
prompt,
|
||||||
(
|
(
|
||||||
"A woman enters the room.\n\n"
|
"A rainy kitchen at night.\n\n"
|
||||||
"wardrobe: Maya = red jacket\nShe sits at the table.\n\n"
|
"Maya: 27, she, red jacket, silver hair.\n\n"
|
||||||
"music: low synth pulse"
|
"Maya enters the room.\n\n"
|
||||||
|
"remove: red jacket\nadd: white shirt underneath\nShe sits at the table."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_assemble_prompt_strips_old_dumas_directives(self):
|
||||||
|
prompt = self.module._assemble_beat_prompt(
|
||||||
|
{
|
||||||
|
"beats": [
|
||||||
|
{
|
||||||
|
"text": (
|
||||||
|
"seconds: 8\n"
|
||||||
|
"continuity: hard cut\n"
|
||||||
|
"ref_mode: every shot\n"
|
||||||
|
"soundscape: soft rain\n"
|
||||||
|
"music: low synth\n"
|
||||||
|
"Maya opens the cupboard.\n"
|
||||||
|
"remove: red jacket"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(prompt, "Maya opens the cupboard.\nremove: red jacket")
|
||||||
|
|
||||||
def test_node_build_prompt_uses_hidden_state(self):
|
def test_node_build_prompt_uses_hidden_state(self):
|
||||||
node = self.module.DumasH3BeatPromptNode()
|
node = self.module.DumasH3BeatPromptNode()
|
||||||
result = node.build_prompt(
|
result = node.build_prompt(
|
||||||
'{"beats":[{"text":"Beat one"},{"text":"Beat two"}]}'
|
'{"scene":"Scene","character_sheet":"Maya: 27, she","beats":[{"text":"Beat one"},{"text":"Beat two"}]}'
|
||||||
)
|
)
|
||||||
self.assertEqual(result, ("Beat one\n\nBeat two",))
|
self.assertEqual(result, ("Scene\n\nMaya: 27, she\n\nBeat one\n\nBeat two",))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -112,8 +112,17 @@ class DumasH3LongVideosUpstreamWrapperTests(unittest.TestCase):
|
|||||||
self.assertIn("first_frame", schema["optional"])
|
self.assertIn("first_frame", schema["optional"])
|
||||||
self.assertIn("ref_image_1", schema["optional"])
|
self.assertIn("ref_image_1", schema["optional"])
|
||||||
self.assertIn("latent_upscale", schema["optional"])
|
self.assertIn("latent_upscale", schema["optional"])
|
||||||
|
self.assertIn("handoff_frames", schema["optional"])
|
||||||
|
self.assertEqual(schema["optional"]["handoff_frames"][1]["default"], 1)
|
||||||
self.assertEqual(node_cls.RETURN_NAMES[0:4], ("images", "audio", "info", "script"))
|
self.assertEqual(node_cls.RETURN_NAMES[0:4], ("images", "audio", "info", "script"))
|
||||||
|
|
||||||
|
def test_handoff_context_claim_names_reference_range(self):
|
||||||
|
upstream = importlib.import_module("dumas_h3_longvideos_upstream")
|
||||||
|
|
||||||
|
self.assertIn("<Picture 2> through <Picture 22>", upstream.handoff_context_claim(2, 22))
|
||||||
|
self.assertIn("no new subjects", upstream.handoff_context_claim(2, 22))
|
||||||
|
self.assertIn("<Picture 5>", upstream.handoff_context_claim(5, 5))
|
||||||
|
|
||||||
|
|
||||||
class _NullContext:
|
class _NullContext:
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user