Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
306816534b | ||
|
|
e098e44bf1 | ||
|
|
769257513b | ||
|
|
ab6ba86d76 | ||
|
|
ecd8ca6336 | ||
|
|
76b88b0c59 | ||
|
|
62fbbdd98e | ||
|
|
85e15c910a | ||
|
|
30400038c2 | ||
|
|
c8ddedbd57 | ||
|
|
6684c5f2e0 | ||
|
|
08630964d5 | ||
|
|
271e7de300 | ||
|
|
130b268d1e | ||
|
|
a2dabaab7e | ||
|
|
b7c3bba199 | ||
|
|
35ec12159e | ||
|
|
58b1c77c19 | ||
|
|
4a0f5cfcd4 | ||
|
|
6a7bec5d03 | ||
|
|
dfead66b9e | ||
|
|
00985b73b3 | ||
|
|
ffae1d87a5 | ||
|
|
74aca203ab | ||
|
|
e08281c12b | ||
|
|
faaeb374e0 | ||
|
|
0bac83a689 | ||
|
|
08365c7df5 | ||
|
|
9cbab72e86 | ||
|
|
e29d01ad24 | ||
|
|
ddd7dbb5af | ||
|
|
1ad68ee9e4 | ||
|
|
e64ad0fae4 | ||
|
|
deb6f1d033 | ||
|
|
72d4d74dd3 | ||
|
|
c42cf436c5 | ||
|
|
eeca789c27 |
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,7 @@
|
|||||||
- Outputs: `images`, `audio`, `info`, `script`, `frames_per_shot`, `total_frames`, `shots`, `video_seconds`, `fps`, `fps_int`, `latent`, `soundscape`
|
- Outputs: `images`, `audio`, `info`, `script`, `frames_per_shot`, `total_frames`, `shots`, `video_seconds`, `fps`, `fps_int`, `latent`, `soundscape`
|
||||||
- First-pass Dumas port of the `MiniMax-H3-Longvideos` sampler, brought in as a local starting point for long-form H3 chaining work.
|
- First-pass Dumas port of the `MiniMax-H3-Longvideos` sampler, brought in as a local starting point for long-form H3 chaining work.
|
||||||
- Keeps the upstream split-beats / handoff / ref-routing behavior close to source so future Dumas-specific improvements can be compared against a known baseline.
|
- Keeps the upstream split-beats / handoff / ref-routing behavior close to source so future Dumas-specific improvements can be compared against a known baseline.
|
||||||
|
- Full user guide: [`H3_LONG_VIDEOS_GUIDE.md`](./H3_LONG_VIDEOS_GUIDE.md)
|
||||||
- Only the canonical `DumasH3LongVideos` node key is exposed now; the older FL2VA/REF2VA alias entries are no longer duplicated in the Add Node menu.
|
- Only the canonical `DumasH3LongVideos` node key is exposed now; the older FL2VA/REF2VA alias entries are no longer duplicated in the Add Node menu.
|
||||||
- Prompt `<Picture N>` tags now map to the actual ref socket numbers you wire, even with gaps such as only `ref_2` and `ref_7` connected.
|
- Prompt `<Picture N>` tags now map to the actual ref socket numbers you wire, even with gaps such as only `ref_2` and `ref_7` connected.
|
||||||
- Character refs now contribute appearance and wardrobe context from the same structured object, while location refs contribute environment context from theirs.
|
- Character refs now contribute appearance and wardrobe context from the same structured object, while location refs contribute environment context from theirs.
|
||||||
@@ -60,12 +61,12 @@
|
|||||||
- Reports the detected H3 base precision / quant format and the relevant compute-capability hints for the current card.
|
- Reports the detected H3 base precision / quant format and the relevant compute-capability hints for the current card.
|
||||||
|
|
||||||
- `Dumas Character Reference`
|
- `Dumas Character Reference`
|
||||||
- Inputs: `image`, `picture_id`, `character_id`, `name`, `alias`, `gender`, `age`, `nationality`, `occupation`, `height_feet`, `height_inches`, `accent`, `description`, `general`, `wardrobe`
|
- Inputs: `image`, `character_id`, `name`, `alias`, `gender`, `age`, `nationality`, `occupation`, `height_feet`, `height_inches`, `accent`, `description`, `general`, `wardrobe`
|
||||||
- Output: `reference`
|
- Output: `reference`
|
||||||
- Builds one structured `REFERENCE` object carrying the conditioning image, identity description, wardrobe, general notes, and simple facts together.
|
- Builds one structured `REFERENCE` object carrying the conditioning image, identity description, wardrobe, general notes, and simple facts together.
|
||||||
|
|
||||||
- `Dumas Location Reference`
|
- `Dumas Location Reference`
|
||||||
- Inputs: `image`, `picture_id`, `location_id`, `name`, `alias`, `description`, `general`
|
- Inputs: `image`, `location_id`, `name`, `alias`, `description`, `general`
|
||||||
- Output: `reference`
|
- Output: `reference`
|
||||||
- Builds one structured `REFERENCE` object for a location/environment so H3 can use the same socket type for both character and scenic refs.
|
- Builds one structured `REFERENCE` object for a location/environment so H3 can use the same socket type for both character and scenic refs.
|
||||||
|
|
||||||
|
|||||||
@@ -119,8 +119,10 @@ For locations:
|
|||||||
- Optional list of alternate match names.
|
- Optional list of alternate match names.
|
||||||
|
|
||||||
- `picture_id`
|
- `picture_id`
|
||||||
- Optional integer representing the intended `<Picture N>` identity.
|
- Optional integer representing the effective `<Picture N>` identity.
|
||||||
- This is authoring metadata, not the final socket position.
|
- It is inferred by consumer nodes such as `Dumas H3 Long Videos` from the
|
||||||
|
connected socket position when that position is known.
|
||||||
|
- Producer nodes do not need a manual `picture_id` input.
|
||||||
|
|
||||||
- `picture_label`
|
- `picture_label`
|
||||||
- Derived convenience text like `<Picture 1>`.
|
- Derived convenience text like `<Picture 1>`.
|
||||||
@@ -167,7 +169,6 @@ Recommended name:
|
|||||||
Inputs:
|
Inputs:
|
||||||
|
|
||||||
- `image`
|
- `image`
|
||||||
- `picture_id`
|
|
||||||
- `character_id`
|
- `character_id`
|
||||||
- `name`
|
- `name`
|
||||||
- `alias`
|
- `alias`
|
||||||
@@ -202,7 +203,6 @@ Recommended name:
|
|||||||
Inputs:
|
Inputs:
|
||||||
|
|
||||||
- `image`
|
- `image`
|
||||||
- `picture_id`
|
|
||||||
- `location_id`
|
- `location_id`
|
||||||
- `name`
|
- `name`
|
||||||
- `alias`
|
- `alias`
|
||||||
@@ -383,4 +383,3 @@ That is the change that removes the current ambiguity between:
|
|||||||
- character identity
|
- character identity
|
||||||
- wardrobe data
|
- wardrobe data
|
||||||
- location / environment description
|
- location / environment description
|
||||||
|
|
||||||
|
|||||||
+522
-277
File diff suppressed because it is too large
Load Diff
+13
-25
@@ -26,10 +26,7 @@ _H3_PLAN_IMAGE_BINDINGS = OrderedDict()
|
|||||||
_H3_PLAN_IMAGE_BINDINGS_CAP = 128
|
_H3_PLAN_IMAGE_BINDINGS_CAP = 128
|
||||||
_H3_PLAN_IMAGE_SLOTS = 9
|
_H3_PLAN_IMAGE_SLOTS = 9
|
||||||
_FOLDER_IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".tiff", ".tif")
|
_FOLDER_IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".tiff", ".tif")
|
||||||
_ANCHOR_STYLE_H3_NOTE = (
|
_ANCHOR_STYLE_H3_NOTE = ""
|
||||||
" Keep this anchor focused on persistent camera language, lighting, texture, environment treatment, and tone; "
|
|
||||||
"do not name characters or describe one-off actions."
|
|
||||||
)
|
|
||||||
_ANCHOR_STYLE_PRESETS = OrderedDict(
|
_ANCHOR_STYLE_PRESETS = OrderedDict(
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
@@ -889,6 +886,8 @@ def normalize_reference(value, picture_id=None, allow_image_fallback=True):
|
|||||||
if image is None and allow_image_fallback:
|
if image is None and allow_image_fallback:
|
||||||
image = value
|
image = value
|
||||||
reference["image"] = image
|
reference["image"] = image
|
||||||
|
existing_picture_id = _coerce_picture_id(reference.get("picture_id"))
|
||||||
|
previous_summary = _ensure_sentence(reference.get("summary") or "")
|
||||||
if picture_id is not None and not reference.get("picture_id"):
|
if picture_id is not None and not reference.get("picture_id"):
|
||||||
reference["picture_id"] = _coerce_picture_id(picture_id)
|
reference["picture_id"] = _coerce_picture_id(picture_id)
|
||||||
reference["picture_label"] = _reference_label(reference.get("picture_id"))
|
reference["picture_label"] = _reference_label(reference.get("picture_id"))
|
||||||
@@ -896,10 +895,16 @@ def normalize_reference(value, picture_id=None, allow_image_fallback=True):
|
|||||||
reference.setdefault("id", _reference_id("", reference.get("name"), reference["kind"]))
|
reference.setdefault("id", _reference_id("", reference.get("name"), reference["kind"]))
|
||||||
reference.setdefault("name", "")
|
reference.setdefault("name", "")
|
||||||
reference["aliases"] = _parse_aliases(reference.get("aliases"))
|
reference["aliases"] = _parse_aliases(reference.get("aliases"))
|
||||||
reference["summary"] = _ensure_sentence(
|
auto_summary_before = _ensure_sentence(
|
||||||
reference.get("summary")
|
_reference_summary(reference["kind"], reference.get("name"), existing_picture_id)
|
||||||
or _reference_summary(reference["kind"], reference.get("name"), reference.get("picture_id"))
|
|
||||||
)
|
)
|
||||||
|
auto_summary_after = _ensure_sentence(
|
||||||
|
_reference_summary(reference["kind"], reference.get("name"), reference.get("picture_id"))
|
||||||
|
)
|
||||||
|
if not previous_summary or previous_summary == auto_summary_before:
|
||||||
|
reference["summary"] = auto_summary_after
|
||||||
|
else:
|
||||||
|
reference["summary"] = previous_summary
|
||||||
reference["description"] = _normalize_free_text(reference.get("description"))
|
reference["description"] = _normalize_free_text(reference.get("description"))
|
||||||
reference["wardrobe"] = _normalize_free_text(reference.get("wardrobe"))
|
reference["wardrobe"] = _normalize_free_text(reference.get("wardrobe"))
|
||||||
reference["general"] = _normalize_free_text(reference.get("general"))
|
reference["general"] = _normalize_free_text(reference.get("general"))
|
||||||
@@ -1614,13 +1619,6 @@ class DumasCharacterReferenceNode:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"image": ("IMAGE", {"tooltip": "Character reference image."}),
|
"image": ("IMAGE", {"tooltip": "Character reference image."}),
|
||||||
"picture_id": (
|
|
||||||
["1", "2", "3", "4", "5", "6", "7", "8", "9"],
|
|
||||||
{
|
|
||||||
"default": "1",
|
|
||||||
"tooltip": "Authoring picture number for this reference.",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"character_id": (
|
"character_id": (
|
||||||
"STRING",
|
"STRING",
|
||||||
{
|
{
|
||||||
@@ -1729,7 +1727,6 @@ class DumasCharacterReferenceNode:
|
|||||||
def build_reference(
|
def build_reference(
|
||||||
self,
|
self,
|
||||||
image,
|
image,
|
||||||
picture_id,
|
|
||||||
character_id,
|
character_id,
|
||||||
name,
|
name,
|
||||||
alias,
|
alias,
|
||||||
@@ -1750,7 +1747,6 @@ class DumasCharacterReferenceNode:
|
|||||||
explicit_id=character_id,
|
explicit_id=character_id,
|
||||||
name=name,
|
name=name,
|
||||||
aliases=alias,
|
aliases=alias,
|
||||||
picture_id=picture_id,
|
|
||||||
description=description,
|
description=description,
|
||||||
wardrobe=wardrobe,
|
wardrobe=wardrobe,
|
||||||
general=general,
|
general=general,
|
||||||
@@ -1782,13 +1778,6 @@ class DumasLocationReferenceNode:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"image": ("IMAGE", {"tooltip": "Location or environment reference image."}),
|
"image": ("IMAGE", {"tooltip": "Location or environment reference image."}),
|
||||||
"picture_id": (
|
|
||||||
["1", "2", "3", "4", "5", "6", "7", "8", "9"],
|
|
||||||
{
|
|
||||||
"default": "1",
|
|
||||||
"tooltip": "Authoring picture number for this reference.",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"location_id": (
|
"location_id": (
|
||||||
"STRING",
|
"STRING",
|
||||||
{
|
{
|
||||||
@@ -1832,7 +1821,7 @@ class DumasLocationReferenceNode:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def build_reference(self, image, picture_id, location_id, name, alias, description, general):
|
def build_reference(self, image, location_id, name, alias, description, general):
|
||||||
return (
|
return (
|
||||||
make_reference(
|
make_reference(
|
||||||
kind="location",
|
kind="location",
|
||||||
@@ -1840,7 +1829,6 @@ class DumasLocationReferenceNode:
|
|||||||
explicit_id=location_id,
|
explicit_id=location_id,
|
||||||
name=name,
|
name=name,
|
||||||
aliases=alias,
|
aliases=alias,
|
||||||
picture_id=picture_id,
|
|
||||||
description=description,
|
description=description,
|
||||||
general=general,
|
general=general,
|
||||||
facts={},
|
facts={},
|
||||||
|
|||||||
@@ -0,0 +1,376 @@
|
|||||||
|
import { app } from "/scripts/app.js";
|
||||||
|
import { applyAdaptiveCanvasOnly } from "../shared/nodes2.mjs";
|
||||||
|
|
||||||
|
const COMFY_CLASS = "DumasH3LongVideos";
|
||||||
|
const STATE_PROPERTY = "dumas_h3_longvideos_section_state";
|
||||||
|
const DOM_WIDGET_NAME = "dumas_h3_longvideos_sections";
|
||||||
|
const MIN_WIDTH = 520;
|
||||||
|
const MIN_HEIGHT = 280;
|
||||||
|
|
||||||
|
const GROUPS = [
|
||||||
|
{
|
||||||
|
id: "prompt",
|
||||||
|
label: "Prompt",
|
||||||
|
defaultCollapsed: false,
|
||||||
|
widgets: ["prompt", "resolution", "megapixels", "beat_split", "anchor_override", "shot_seconds", "plan_only", "fps"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "refs",
|
||||||
|
label: "Refs",
|
||||||
|
defaultCollapsed: true,
|
||||||
|
widgets: ["ref_mode", "ref_image_size", "ref_noise_aug", "character_memory", "trim_seam", "vary_seed_per_shot", "handoff_offset"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sampling",
|
||||||
|
label: "Sampling",
|
||||||
|
defaultCollapsed: true,
|
||||||
|
widgets: [
|
||||||
|
"steps", "cfg", "sampler_name", "scheduler", "seed",
|
||||||
|
"apply_model_sampling", "shift_video", "shift_audio",
|
||||||
|
"vram_headroom_gb", "allow_res_backoff",
|
||||||
|
"decode_tile_frames", "decode_tile_size",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "audio",
|
||||||
|
label: "Audio",
|
||||||
|
defaultCollapsed: true,
|
||||||
|
widgets: [
|
||||||
|
"global_soundscape", "non_diegetic_music", "auto_soundscape",
|
||||||
|
"auto_silence_nonspeech", "allow_nonspeech_vocals",
|
||||||
|
"mute_nonspeech_audio", "mute_fade_ms",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "scene",
|
||||||
|
label: "Scene Logic",
|
||||||
|
defaultCollapsed: true,
|
||||||
|
widgets: [
|
||||||
|
"auto_wardrobe", "auto_props", "prevent_nudity", "exposed_terms",
|
||||||
|
"anatomy_guard", "subject_count_guard", "lock_restraints",
|
||||||
|
"contact_guard", "motion_guard", "solidity_guard",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "finish",
|
||||||
|
label: "Upscale/Detail",
|
||||||
|
defaultCollapsed: true,
|
||||||
|
widgets: [
|
||||||
|
"upscale", "upscale_model", "upscale_target_short_edge", "upscale_batch",
|
||||||
|
"detail_pass", "detail_sampler_name", "detail_scheduler", "detail_steps", "detail_denoise",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "overlay",
|
||||||
|
label: "Overlays",
|
||||||
|
defaultCollapsed: true,
|
||||||
|
widgets: [
|
||||||
|
"watermark_text", "watermark_position", "watermark_size", "watermark_opacity", "watermark_margin",
|
||||||
|
"intro_text", "intro_position", "intro_seconds", "intro_fade", "intro_size",
|
||||||
|
"overlay_font", "overlay_stroke",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function injectCSS() {
|
||||||
|
if (document.getElementById("dumas-h3lv-sections-css")) return;
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.id = "dumas-h3lv-sections-css";
|
||||||
|
style.textContent = `
|
||||||
|
.dh3lv-sections {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 10px 6px;
|
||||||
|
color: #e6e7eb;
|
||||||
|
font: 12px/1.35 "Segoe UI", sans-serif;
|
||||||
|
pointer-events: auto;
|
||||||
|
background: linear-gradient(180deg, rgba(33, 36, 42, 0.96), rgba(22, 24, 29, 0.96));
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
.dh3lv-sections-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.dh3lv-sections-title {
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #9da5b1;
|
||||||
|
}
|
||||||
|
.dh3lv-sections-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.dh3lv-sections-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.dh3lv-chip,
|
||||||
|
.dh3lv-action {
|
||||||
|
appearance: none;
|
||||||
|
border: 1px solid #464d59;
|
||||||
|
background: #262c35;
|
||||||
|
color: #d7dce3;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 5px 9px;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
.dh3lv-chip[data-open="true"] {
|
||||||
|
background: #d96f2b;
|
||||||
|
border-color: #f09358;
|
||||||
|
color: #fff7f0;
|
||||||
|
}
|
||||||
|
.dh3lv-chip:hover,
|
||||||
|
.dh3lv-action:hover {
|
||||||
|
filter: brightness(1.06);
|
||||||
|
}
|
||||||
|
.dh3lv-count {
|
||||||
|
opacity: 0.78;
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultState() {
|
||||||
|
const state = {};
|
||||||
|
for (const group of GROUPS) state[group.id] = !group.defaultCollapsed;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseState(value) {
|
||||||
|
let parsed = value;
|
||||||
|
if (typeof parsed === "string") {
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(parsed);
|
||||||
|
} catch (_error) {
|
||||||
|
parsed = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const base = defaultState();
|
||||||
|
if (!parsed || typeof parsed !== "object") return base;
|
||||||
|
for (const group of GROUPS) {
|
||||||
|
if (typeof parsed[group.id] === "boolean") base[group.id] = parsed[group.id];
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readState(node) {
|
||||||
|
return parseState(node.properties?.[STATE_PROPERTY] || node._dh3lvSectionState || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeState(node, state) {
|
||||||
|
const normalized = parseState(state);
|
||||||
|
const serialized = JSON.stringify(normalized);
|
||||||
|
node._dh3lvSectionState = serialized;
|
||||||
|
node.properties = node.properties || {};
|
||||||
|
node.properties[STATE_PROPERTY] = serialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findWidget(node, name) {
|
||||||
|
return (node.widgets || []).find((widget) => widget?.name === name) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInteractiveTarget(target) {
|
||||||
|
return !!target?.closest?.("button, input, textarea, select, label");
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopCanvasEvent(event) {
|
||||||
|
if (isInteractiveTarget(event.target)) event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopCanvasKeyboard(event) {
|
||||||
|
if (isInteractiveTarget(event.target)) event.stopImmediatePropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWidgetHidden(widget, hidden) {
|
||||||
|
if (!widget) return;
|
||||||
|
if (!widget._dh3lvOriginal) {
|
||||||
|
widget._dh3lvOriginal = {
|
||||||
|
type: widget.type,
|
||||||
|
computeSize: widget.computeSize,
|
||||||
|
hidden: widget.hidden,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (hidden) {
|
||||||
|
widget.type = "hidden";
|
||||||
|
widget.hidden = true;
|
||||||
|
widget.computeSize = () => [0, -4];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
widget.type = widget._dh3lvOriginal.type;
|
||||||
|
widget.hidden = !!widget._dh3lvOriginal.hidden;
|
||||||
|
widget.computeSize = widget._dh3lvOriginal.computeSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyVisibility(node) {
|
||||||
|
const state = readState(node);
|
||||||
|
for (const group of GROUPS) {
|
||||||
|
for (const name of group.widgets) {
|
||||||
|
const widget = findWidget(node, name);
|
||||||
|
if (!widget || widget.name === DOM_WIDGET_NAME) continue;
|
||||||
|
setWidgetHidden(widget, !state[group.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeNode(node) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const size = node.computeSize?.();
|
||||||
|
if (Array.isArray(size)) {
|
||||||
|
node.size[0] = Math.max(MIN_WIDTH, size[0] || 0, node.size?.[0] || 0);
|
||||||
|
node.size[1] = Math.max(MIN_HEIGHT, size[1] || 0);
|
||||||
|
}
|
||||||
|
node.setDirtyCanvas?.(true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderToolbar(node) {
|
||||||
|
const ui = node._dh3lvUI;
|
||||||
|
if (!ui) return;
|
||||||
|
const state = readState(node);
|
||||||
|
ui.list.innerHTML = "";
|
||||||
|
for (const group of GROUPS) {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "dh3lv-chip";
|
||||||
|
button.dataset.open = state[group.id] ? "true" : "false";
|
||||||
|
button.textContent = state[group.id] ? `Hide ${group.label}` : `Show ${group.label}`;
|
||||||
|
|
||||||
|
const count = document.createElement("span");
|
||||||
|
count.className = "dh3lv-count";
|
||||||
|
count.textContent = String(group.widgets.filter((name) => findWidget(node, name)).length);
|
||||||
|
button.appendChild(count);
|
||||||
|
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const next = readState(node);
|
||||||
|
next[group.id] = !next[group.id];
|
||||||
|
writeState(node, next);
|
||||||
|
applyVisibility(node);
|
||||||
|
renderToolbar(node);
|
||||||
|
resizeNode(node);
|
||||||
|
});
|
||||||
|
ui.list.appendChild(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAll(node, open) {
|
||||||
|
const next = {};
|
||||||
|
for (const group of GROUPS) next[group.id] = !!open;
|
||||||
|
writeState(node, next);
|
||||||
|
applyVisibility(node);
|
||||||
|
renderToolbar(node);
|
||||||
|
resizeNode(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupNode(node) {
|
||||||
|
if (node._dh3lvUI) return;
|
||||||
|
injectCSS();
|
||||||
|
writeState(node, readState(node));
|
||||||
|
|
||||||
|
const root = document.createElement("div");
|
||||||
|
root.className = "dh3lv-sections";
|
||||||
|
|
||||||
|
const head = document.createElement("div");
|
||||||
|
head.className = "dh3lv-sections-head";
|
||||||
|
|
||||||
|
const title = document.createElement("div");
|
||||||
|
title.className = "dh3lv-sections-title";
|
||||||
|
title.textContent = "Sections";
|
||||||
|
|
||||||
|
const actions = document.createElement("div");
|
||||||
|
actions.className = "dh3lv-sections-actions";
|
||||||
|
|
||||||
|
const expandAll = document.createElement("button");
|
||||||
|
expandAll.type = "button";
|
||||||
|
expandAll.className = "dh3lv-action";
|
||||||
|
expandAll.textContent = "Expand All";
|
||||||
|
expandAll.addEventListener("click", () => setAll(node, true));
|
||||||
|
|
||||||
|
const collapseAll = document.createElement("button");
|
||||||
|
collapseAll.type = "button";
|
||||||
|
collapseAll.className = "dh3lv-action";
|
||||||
|
collapseAll.textContent = "Collapse Extras";
|
||||||
|
collapseAll.addEventListener("click", () => {
|
||||||
|
const next = defaultState();
|
||||||
|
writeState(node, next);
|
||||||
|
applyVisibility(node);
|
||||||
|
renderToolbar(node);
|
||||||
|
resizeNode(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
actions.append(expandAll, collapseAll);
|
||||||
|
head.append(title, actions);
|
||||||
|
|
||||||
|
const list = document.createElement("div");
|
||||||
|
list.className = "dh3lv-sections-list";
|
||||||
|
root.append(head, list);
|
||||||
|
root.addEventListener("pointerdown", stopCanvasEvent);
|
||||||
|
root.addEventListener("mousedown", stopCanvasEvent);
|
||||||
|
root.addEventListener("click", stopCanvasEvent);
|
||||||
|
root.addEventListener("dblclick", stopCanvasEvent);
|
||||||
|
root.addEventListener("keydown", stopCanvasKeyboard, true);
|
||||||
|
|
||||||
|
node._dh3lvUI = { root, list };
|
||||||
|
const widget = node.addDOMWidget(DOM_WIDGET_NAME, "custom", root, {
|
||||||
|
getValue: () => null,
|
||||||
|
setValue: () => {},
|
||||||
|
serialize: false,
|
||||||
|
getMinHeight: () => 52,
|
||||||
|
hideOnZoom: false,
|
||||||
|
});
|
||||||
|
applyAdaptiveCanvasOnly(widget);
|
||||||
|
const widgets = node.widgets || [];
|
||||||
|
const index = widgets.indexOf(widget);
|
||||||
|
if (index > 0) {
|
||||||
|
widgets.splice(index, 1);
|
||||||
|
widgets.unshift(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyVisibility(node);
|
||||||
|
renderToolbar(node);
|
||||||
|
resizeNode(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.registerExtension({
|
||||||
|
name: "Dumas.H3LongVideosSections",
|
||||||
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||||
|
if (nodeData?.name !== COMFY_CLASS) return;
|
||||||
|
|
||||||
|
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
|
||||||
|
nodeType.prototype.onNodeCreated = function onNodeCreated() {
|
||||||
|
const result = originalOnNodeCreated?.apply(this, arguments);
|
||||||
|
setupNode(this);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalConfigure = nodeType.prototype.onConfigure;
|
||||||
|
nodeType.prototype.onConfigure = function onConfigure() {
|
||||||
|
const result = originalConfigure?.apply(this, arguments);
|
||||||
|
setupNode(this);
|
||||||
|
writeState(this, readState(this));
|
||||||
|
applyVisibility(this);
|
||||||
|
renderToolbar(this);
|
||||||
|
resizeNode(this);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalSerialize = nodeType.prototype.onSerialize;
|
||||||
|
nodeType.prototype.onSerialize = function onSerialize(o) {
|
||||||
|
writeState(this, readState(this));
|
||||||
|
const result = originalSerialize?.apply(this, arguments);
|
||||||
|
if (o && this.properties?.[STATE_PROPERTY]) {
|
||||||
|
o.properties = o.properties || {};
|
||||||
|
o.properties[STATE_PROPERTY] = this.properties[STATE_PROPERTY];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "dumasnodes"
|
name = "dumasnodes"
|
||||||
description = "Dumas-branded generic utility nodes for ComfyUI, starting with JSON helpers."
|
description = "Dumas-branded generic utility nodes for ComfyUI, starting with JSON helpers."
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
|||||||
@@ -160,6 +160,80 @@ 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):
|
||||||
|
note = self.module._format_timing_note([
|
||||||
|
{
|
||||||
|
"shot": 1,
|
||||||
|
"total": 12.4,
|
||||||
|
"retry_elapsed": 1.2,
|
||||||
|
"attempts": 2,
|
||||||
|
"sample": 8.0,
|
||||||
|
"detail_sample": 0.5,
|
||||||
|
"decode_video": 2.1,
|
||||||
|
"decode_audio": 0.4,
|
||||||
|
"cleanup": 0.2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shot": 2,
|
||||||
|
"total": 7.6,
|
||||||
|
"retry_elapsed": 0.0,
|
||||||
|
"attempts": 1,
|
||||||
|
"sample": 6.5,
|
||||||
|
"decode_video": 0.5,
|
||||||
|
"decode_audio": 0.3,
|
||||||
|
"cleanup": 0.1,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
self.assertIn("timing: 2 shot(s) total 20.0s", note)
|
||||||
|
self.assertIn("sample 14.5s", note)
|
||||||
|
self.assertIn("decode video 2.6s", note)
|
||||||
|
self.assertIn("decode audio 0.7s", note)
|
||||||
|
self.assertIn("cleanup 0.3s", note)
|
||||||
|
self.assertIn("retry elapsed 1.2s", note)
|
||||||
|
self.assertIn("detail 0.5s", note)
|
||||||
|
self.assertIn("retries 1", note)
|
||||||
|
self.assertIn("slowest shot 1 12.4s", note)
|
||||||
|
|
||||||
def test_detail_pass_refines_video_but_preserves_audio(self):
|
def test_detail_pass_refines_video_but_preserves_audio(self):
|
||||||
class FakeTensor:
|
class FakeTensor:
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
@@ -254,6 +328,107 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
else:
|
else:
|
||||||
self.module.comfy.nested_tensor.NestedTensor = original_nested
|
self.module.comfy.nested_tensor.NestedTensor = original_nested
|
||||||
|
|
||||||
|
def test_detail_pass_decodes_audio_before_video_and_cleans_up(self):
|
||||||
|
class FakeTensor:
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def detach(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def to(self, *args, **kwargs):
|
||||||
|
return self
|
||||||
|
|
||||||
|
class FakeNestedTensor:
|
||||||
|
def __init__(self, parts):
|
||||||
|
self._parts = tuple(parts)
|
||||||
|
self.is_nested = True
|
||||||
|
|
||||||
|
def unbind(self):
|
||||||
|
return self._parts
|
||||||
|
|
||||||
|
order = []
|
||||||
|
first_out = {"samples": FakeNestedTensor((FakeTensor("v1"), FakeTensor("a1")))}
|
||||||
|
second_out = {"samples": FakeNestedTensor((FakeTensor("v2"), FakeTensor("a2")))}
|
||||||
|
|
||||||
|
original_common_ksampler = self.module.nodes.common_ksampler
|
||||||
|
original_build = self.module._build_shot_conditioning
|
||||||
|
original_evict = self.module._evict_all_but
|
||||||
|
original_decode_video = self.module._decode_video
|
||||||
|
original_decode_audio = self.module._decode_audio
|
||||||
|
original_cleanup = self.module._deep_cleanup
|
||||||
|
original_nested = getattr(self.module.comfy.nested_tensor, "NestedTensor", None)
|
||||||
|
try:
|
||||||
|
self.module.comfy.nested_tensor.NestedTensor = FakeNestedTensor
|
||||||
|
|
||||||
|
def common_ksampler(*args, **kwargs):
|
||||||
|
order.append("detail_sample" if len(order) else "sample")
|
||||||
|
return (first_out if len([x for x in order if x.endswith("sample")]) == 1 else second_out,)
|
||||||
|
|
||||||
|
def decode_audio(_vae, out_latent):
|
||||||
|
order.append("audio")
|
||||||
|
self.assertIs(out_latent, first_out)
|
||||||
|
return out_latent
|
||||||
|
|
||||||
|
def decode_video(_vae, out_latent, *_args, **_kwargs):
|
||||||
|
order.append("video")
|
||||||
|
self.assertIsNot(out_latent, first_out)
|
||||||
|
self.assertIs(out_latent["samples"].unbind()[0], second_out["samples"].unbind()[0])
|
||||||
|
return out_latent
|
||||||
|
|
||||||
|
def cleanup():
|
||||||
|
order.append("cleanup")
|
||||||
|
|
||||||
|
self.module.nodes.common_ksampler = common_ksampler
|
||||||
|
self.module._build_shot_conditioning = lambda *_args, **_kwargs: (
|
||||||
|
"cond",
|
||||||
|
{"samples": FakeNestedTensor((FakeTensor("basev"), FakeTensor("basea")))},
|
||||||
|
)
|
||||||
|
self.module._evict_all_but = lambda *_args, **_kwargs: None
|
||||||
|
self.module._decode_video = decode_video
|
||||||
|
self.module._decode_audio = decode_audio
|
||||||
|
self.module._deep_cleanup = cleanup
|
||||||
|
|
||||||
|
self.module.H3LongVideos()._render(
|
||||||
|
model=object(),
|
||||||
|
clip=types.SimpleNamespace(
|
||||||
|
tokenize=lambda text, **kwargs: text,
|
||||||
|
encode_from_tokens_scheduled=lambda tokens: tokens,
|
||||||
|
),
|
||||||
|
vae=object(),
|
||||||
|
audio_vae=object(),
|
||||||
|
negative="negative",
|
||||||
|
prompt="beat",
|
||||||
|
w=128,
|
||||||
|
h=64,
|
||||||
|
ln=24,
|
||||||
|
fps=24,
|
||||||
|
tiled=False,
|
||||||
|
sa=(123, 20, 1.0, "res_multistep", "simple", 1.0),
|
||||||
|
handoff=None,
|
||||||
|
detail_pass=True,
|
||||||
|
detail_sampler_name="euler",
|
||||||
|
detail_scheduler="beta",
|
||||||
|
detail_steps=5,
|
||||||
|
detail_denoise=0.4,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(order[0], "sample")
|
||||||
|
self.assertEqual(order[1], "detail_sample")
|
||||||
|
self.assertLess(order.index("audio"), order.index("video"))
|
||||||
|
self.assertEqual(order[-1], "cleanup")
|
||||||
|
finally:
|
||||||
|
self.module.nodes.common_ksampler = original_common_ksampler
|
||||||
|
self.module._build_shot_conditioning = original_build
|
||||||
|
self.module._evict_all_but = original_evict
|
||||||
|
self.module._decode_video = original_decode_video
|
||||||
|
self.module._decode_audio = original_decode_audio
|
||||||
|
self.module._deep_cleanup = original_cleanup
|
||||||
|
if original_nested is None:
|
||||||
|
delattr(self.module.comfy.nested_tensor, "NestedTensor")
|
||||||
|
else:
|
||||||
|
self.module.comfy.nested_tensor.NestedTensor = original_nested
|
||||||
|
|
||||||
def test_detail_pass_treats_falsey_strings_as_disabled(self):
|
def test_detail_pass_treats_falsey_strings_as_disabled(self):
|
||||||
calls = []
|
calls = []
|
||||||
original_common_ksampler = self.module.nodes.common_ksampler
|
original_common_ksampler = self.module.nodes.common_ksampler
|
||||||
@@ -407,6 +582,91 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(dropped, [])
|
self.assertEqual(dropped, [])
|
||||||
|
|
||||||
|
def test_resolve_prompt_refs_where_tagged_mode_stays_tag_only(self):
|
||||||
|
refs = [
|
||||||
|
{"kind": "character", "image": "img1", "name": "Mara"},
|
||||||
|
{"kind": "location", "image": "img2", "name": "Hangar"},
|
||||||
|
]
|
||||||
|
|
||||||
|
text, references, dropped = self.module.resolve_prompt_refs(
|
||||||
|
"Mara waits in the hangar near <Picture 2>.",
|
||||||
|
refs,
|
||||||
|
include_named=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(text, "Mara waits in the hangar near <Picture 1>.")
|
||||||
|
self.assertEqual([self.module._reference_image(ref) for ref in references], ["img2"])
|
||||||
|
self.assertEqual(dropped, [])
|
||||||
|
|
||||||
|
def test_resolve_tag_driven_prompt_refs_keeps_untagged_shot_on_handoff(self):
|
||||||
|
refs = [
|
||||||
|
{"kind": "character", "image": "img1", "name": "Mara"},
|
||||||
|
{"kind": "location", "image": "img2", "name": "Hangar"},
|
||||||
|
]
|
||||||
|
|
||||||
|
text, references, dropped = self.module.resolve_tag_driven_prompt_refs(
|
||||||
|
"Mara waits in the hangar.",
|
||||||
|
refs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(text, "Mara waits in the hangar.")
|
||||||
|
self.assertEqual(references, [])
|
||||||
|
self.assertEqual(dropped, [])
|
||||||
|
|
||||||
|
def test_resolve_tag_driven_prompt_refs_keeps_named_refs_on_tagged_shot(self):
|
||||||
|
refs = [
|
||||||
|
{"kind": "character", "image": "img1", "name": "Mara"},
|
||||||
|
{"kind": "location", "image": "img2", "name": "Hangar"},
|
||||||
|
]
|
||||||
|
|
||||||
|
text, references, dropped = self.module.resolve_tag_driven_prompt_refs(
|
||||||
|
"Mara waits in the hangar near <Picture 2>.",
|
||||||
|
refs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(text, "Mara waits in the hangar near <Picture 1>.")
|
||||||
|
self.assertEqual(
|
||||||
|
[self.module._reference_image(ref) for ref in references],
|
||||||
|
["img2", "img1"],
|
||||||
|
)
|
||||||
|
self.assertEqual(dropped, [])
|
||||||
|
|
||||||
|
def test_resolve_shot_references_uses_named_characters_without_picture_tags(self):
|
||||||
|
refs = [
|
||||||
|
{"kind": "character", "image": "img1", "name": "Mara"},
|
||||||
|
{"kind": "character", "image": "img2", "name": "Jon"},
|
||||||
|
{"kind": "location", "image": "img3", "name": "Hangar"},
|
||||||
|
]
|
||||||
|
|
||||||
|
text, references, dropped, shot_tag_driven, mode_eff = self.module.resolve_shot_references(
|
||||||
|
"[Generation 1] Mara crosses the hangar.",
|
||||||
|
refs,
|
||||||
|
"auto ref2v",
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(text, "[Generation 1] Mara crosses the hangar.")
|
||||||
|
self.assertEqual([self.module._reference_image(ref) for ref in references], ["img1"])
|
||||||
|
self.assertEqual(dropped, [])
|
||||||
|
self.assertFalse(shot_tag_driven)
|
||||||
|
self.assertEqual(mode_eff, "auto ref2v")
|
||||||
|
|
||||||
|
def test_resolve_prompt_refs_prioritizes_characters_before_locations(self):
|
||||||
|
refs = [
|
||||||
|
{"kind": "location", "image": "img1", "name": "Hangar"},
|
||||||
|
{"kind": "character", "image": "img2", "name": "Mara"},
|
||||||
|
]
|
||||||
|
|
||||||
|
text, references, dropped = self.module.resolve_prompt_refs(
|
||||||
|
"[Generation 1] Mara waits in the hangar.",
|
||||||
|
refs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(text, "[Generation 1] Mara waits in the hangar.")
|
||||||
|
self.assertEqual([self.module._reference_image(ref) for ref in references], ["img2", "img1"])
|
||||||
|
self.assertEqual(dropped, [])
|
||||||
|
|
||||||
def test_shot_references_uses_all_connected_sparse_slots(self):
|
def test_shot_references_uses_all_connected_sparse_slots(self):
|
||||||
refs = [
|
refs = [
|
||||||
None,
|
None,
|
||||||
@@ -426,17 +686,75 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
["img2", "img4", "img7"],
|
["img2", "img4", "img7"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_annotate_script_refs_handles_named_characters_and_locations(self):
|
||||||
|
refs = [
|
||||||
|
{"kind": "character", "image": "img1", "name": "Mara"},
|
||||||
|
{"kind": "location", "image": "img2", "name": "Hangar"},
|
||||||
|
]
|
||||||
|
|
||||||
|
report = self.module.annotate_script_refs(
|
||||||
|
["Mara waits in the Hangar.", "Nobody else is here."],
|
||||||
|
refs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn("# shot 1 refs: Picture 1 Mara (by name); Picture 2 Hangar (by name)", report)
|
||||||
|
self.assertIn("# shot 2 refs: none", report)
|
||||||
|
|
||||||
|
def test_annotate_script_debug_groups_each_prompt_with_its_beat_info(self):
|
||||||
|
refs = [
|
||||||
|
{"kind": "character", "image": "img1", "name": "Mara"},
|
||||||
|
{"kind": "location", "image": "img2", "name": "Hangar"},
|
||||||
|
]
|
||||||
|
|
||||||
|
report = self.module.annotate_script_debug(
|
||||||
|
["[Generation 1] Mara waits in the Hangar.", "[Generation 2] Nobody else is here."],
|
||||||
|
[1],
|
||||||
|
"auto",
|
||||||
|
refs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn("Prompt 1\n[Generation 1] Mara waits in the Hangar.", report)
|
||||||
|
self.assertIn("Beat 1 info\nAnatomy guard: injected into this prompt", report)
|
||||||
|
self.assertIn("References used: <Picture 1> Mara (matched by name); <Picture 2> Hangar (matched by name)", report)
|
||||||
|
self.assertIn("Prompt 2\n[Generation 2] Nobody else is here.", report)
|
||||||
|
self.assertIn("Beat 2 info\nAnatomy guard: not injected for this prompt\nReferences used: none", report)
|
||||||
|
|
||||||
def test_input_types_expose_nine_ref_slots(self):
|
def test_input_types_expose_nine_ref_slots(self):
|
||||||
optional = self.module.H3LongVideos.INPUT_TYPES()["optional"]
|
optional = self.module.H3LongVideos.INPUT_TYPES()["optional"]
|
||||||
|
|
||||||
for index in range(1, 10):
|
for index in range(1, 10):
|
||||||
self.assertIn(f"ref_{index}", optional)
|
self.assertIn(f"ref_{index}", optional)
|
||||||
|
|
||||||
def test_input_types_keep_legacy_ref_image_aliases(self):
|
names = list(optional)
|
||||||
|
ref_positions = [names.index(f"ref_{index}") for index in range(1, 10)]
|
||||||
|
self.assertEqual(ref_positions, list(range(ref_positions[0], ref_positions[0] + 9)))
|
||||||
|
|
||||||
|
def test_input_types_do_not_expose_legacy_ref_image_aliases(self):
|
||||||
optional = self.module.H3LongVideos.INPUT_TYPES()["optional"]
|
optional = self.module.H3LongVideos.INPUT_TYPES()["optional"]
|
||||||
|
|
||||||
for index in range(1, 10):
|
for index in range(1, 10):
|
||||||
self.assertIn(f"ref_image_{index}", optional)
|
self.assertNotIn(f"ref_image_{index}", optional)
|
||||||
|
self.assertNotIn("per_beat_length", optional)
|
||||||
|
self.assertNotIn("cleanup_between_shots", optional)
|
||||||
|
|
||||||
|
def test_shot_seconds_tooltip_describes_ceiling_behavior(self):
|
||||||
|
optional = self.module.H3LongVideos.INPUT_TYPES()["optional"]
|
||||||
|
tooltip = optional["shot_seconds"][1]["tooltip"]
|
||||||
|
|
||||||
|
self.assertIn("GLOBAL per-shot maximum", tooltip)
|
||||||
|
self.assertIn("A beat's own `seconds:` directive can still ask for less", tooltip)
|
||||||
|
self.assertIn("honoring it; may spill to system RAM (slow) or OOM", tooltip)
|
||||||
|
|
||||||
|
def test_resolve_shot_frames_honors_forced_request_over_budget(self):
|
||||||
|
original_estimate_shot_frames = self.module.estimate_shot_frames
|
||||||
|
try:
|
||||||
|
self.module.estimate_shot_frames = lambda *_args, **_kwargs: 73
|
||||||
|
frames, note = self.module.resolve_shot_frames(10.0, 24, 16.0, 8.0, 1.5)
|
||||||
|
|
||||||
|
self.assertEqual(frames, 243)
|
||||||
|
self.assertIn("honoring it", note)
|
||||||
|
finally:
|
||||||
|
self.module.estimate_shot_frames = original_estimate_shot_frames
|
||||||
|
|
||||||
def test_run_defaults_match_declared_ref_widget_defaults(self):
|
def test_run_defaults_match_declared_ref_widget_defaults(self):
|
||||||
node = self.module.H3LongVideos()
|
node = self.module.H3LongVideos()
|
||||||
@@ -497,7 +815,54 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
self.assertIn("Persistent wardrobe/style for Mara: red jacket.", context)
|
self.assertIn("Persistent wardrobe/style for Mara: red jacket.", context)
|
||||||
self.assertIn("Character notes for Mara: wears a long grey coat.", context)
|
self.assertIn("Character notes for Mara: wears a long grey coat.", context)
|
||||||
|
|
||||||
def test_run_uses_legacy_ref_image_inputs_when_new_slots_are_empty(self):
|
def test_reference_context_matches_location_names_without_picture_tag(self):
|
||||||
|
refs = [
|
||||||
|
{"kind": "location", "image": "img2", "name": "Hangar", "aliases": ["loading bay"], "description": "wet concrete floor", "general": "cold industrial lighting"},
|
||||||
|
]
|
||||||
|
|
||||||
|
context = self.module._reference_context_for_text(
|
||||||
|
"[Generation 1] They argue in the hangar near the loading bay.",
|
||||||
|
refs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn("Location context for Hangar: wet concrete floor.", context)
|
||||||
|
self.assertIn("Location notes for Hangar: cold industrial lighting.", context)
|
||||||
|
|
||||||
|
def test_reference_context_uses_resolved_picture_numbers_for_per_beat_refs(self):
|
||||||
|
refs = [
|
||||||
|
{"kind": "character", "image": "img1", "name": "Bill", "description": "very tall", "facts": {"age": "25"}},
|
||||||
|
{"kind": "location", "image": "img2", "name": "Pub", "description": "warm wood bar"},
|
||||||
|
]
|
||||||
|
|
||||||
|
rewritten, resolved_refs, dropped = self.module.resolve_prompt_refs(
|
||||||
|
"[Generation 1] Bill leans on <Picture 2>.",
|
||||||
|
refs,
|
||||||
|
)
|
||||||
|
context = self.module._reference_context_for_text(
|
||||||
|
rewritten,
|
||||||
|
refs,
|
||||||
|
resolved_refs=resolved_refs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(dropped, [])
|
||||||
|
self.assertIn("Character facts for <Picture 2> Bill: 25 years old.", context)
|
||||||
|
self.assertIn("Persistent appearance for <Picture 2> Bill: very tall.", context)
|
||||||
|
self.assertIn("Location context for <Picture 1> Pub: warm wood bar.", context)
|
||||||
|
|
||||||
|
def test_reference_context_skips_ambiguous_name_matches(self):
|
||||||
|
refs = [
|
||||||
|
{"kind": "character", "image": "img1", "name": "Alex", "description": "short dark hair"},
|
||||||
|
{"kind": "character", "image": "img2", "name": "Alex", "description": "tall blond hair"},
|
||||||
|
]
|
||||||
|
|
||||||
|
context = self.module._reference_context_for_text(
|
||||||
|
"[Generation 1] Alex enters the room.",
|
||||||
|
refs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(context, "")
|
||||||
|
|
||||||
|
def test_run_uses_reference_slots_directly(self):
|
||||||
calls = {}
|
calls = {}
|
||||||
original_parse_resolution = self.module.parse_resolution
|
original_parse_resolution = self.module.parse_resolution
|
||||||
original_connected_refs = self.module._connected_refs
|
original_connected_refs = self.module._connected_refs
|
||||||
@@ -561,13 +926,13 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
scheduler="simple",
|
scheduler="simple",
|
||||||
seed=1,
|
seed=1,
|
||||||
plan_only=True,
|
plan_only=True,
|
||||||
ref_image_1={"image": "legacy-1"},
|
ref_1={"image": "live-1"},
|
||||||
ref_image_3={"image": "legacy-3"},
|
ref_3={"image": "live-3"},
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(calls["refs"][0]["image"], "legacy-1")
|
self.assertEqual(calls["refs"][0]["image"], "live-1")
|
||||||
self.assertIsNone(calls["refs"][1])
|
self.assertIsNone(calls["refs"][1])
|
||||||
self.assertEqual(calls["refs"][2]["image"], "legacy-3")
|
self.assertEqual(calls["refs"][2]["image"], "live-3")
|
||||||
self.assertEqual(result[2].count("ref2va: 2 reference image(s)"), 1)
|
self.assertEqual(result[2].count("ref2va: 2 reference image(s)"), 1)
|
||||||
finally:
|
finally:
|
||||||
self.module.parse_resolution = original_parse_resolution
|
self.module.parse_resolution = original_parse_resolution
|
||||||
@@ -608,6 +973,51 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
self.assertIn("Persistent appearance for Mara: silver hair.", context)
|
self.assertIn("Persistent appearance for Mara: silver hair.", context)
|
||||||
self.assertIn("Persistent wardrobe/style for Mara: red jacket.", context)
|
self.assertIn("Persistent wardrobe/style for Mara: red jacket.", context)
|
||||||
|
|
||||||
|
def test_reference_context_can_skip_character_wardrobe_when_live_memory_is_explicit(self):
|
||||||
|
refs = [
|
||||||
|
{"kind": "character", "image": "img1", "name": "Mara", "description": "silver hair", "wardrobe": "red jacket"},
|
||||||
|
]
|
||||||
|
|
||||||
|
context = self.module._reference_context_for_text(
|
||||||
|
"[Generation 1] Mara walks into the room.",
|
||||||
|
refs,
|
||||||
|
include_character_wardrobe=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn("Persistent appearance for Mara: silver hair.", context)
|
||||||
|
self.assertNotIn("Persistent wardrobe/style for Mara: red jacket.", context)
|
||||||
|
|
||||||
|
def test_resolve_prompt_refs_adds_named_location_refs(self):
|
||||||
|
refs = [
|
||||||
|
{"kind": "location", "image": "img2", "name": "Hangar", "description": "wet concrete floor"},
|
||||||
|
]
|
||||||
|
|
||||||
|
rewritten, matched, dropped = self.module.resolve_prompt_refs(
|
||||||
|
"[Generation 1] They wait in the hangar.",
|
||||||
|
refs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(rewritten, "[Generation 1] They wait in the hangar.")
|
||||||
|
self.assertEqual(dropped, [])
|
||||||
|
self.assertEqual(len(matched), 1)
|
||||||
|
self.assertEqual(matched[0]["name"], "Hangar")
|
||||||
|
|
||||||
|
def test_resolve_tagged_refs_drops_reference_without_image(self):
|
||||||
|
refs = [
|
||||||
|
{"kind": "character", "image": None, "name": "Mara"},
|
||||||
|
{"kind": "character", "image": "img2", "name": "Jon"},
|
||||||
|
]
|
||||||
|
|
||||||
|
rewritten, matched, dropped = self.module.resolve_tagged_refs(
|
||||||
|
"[Generation 1] <Picture 1> faces <Picture 2>.",
|
||||||
|
refs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(rewritten, "[Generation 1] faces <Picture 1>.")
|
||||||
|
self.assertEqual(dropped, [1])
|
||||||
|
self.assertEqual(len(matched), 1)
|
||||||
|
self.assertEqual(matched[0]["name"], "Jon")
|
||||||
|
|
||||||
def test_reference_context_injects_immediately_after_generation_label(self):
|
def test_reference_context_injects_immediately_after_generation_label(self):
|
||||||
block = (
|
block = (
|
||||||
"[Generation 1] Classic sitcom lighting and staging. "
|
"[Generation 1] Classic sitcom lighting and staging. "
|
||||||
@@ -755,7 +1165,7 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
module.quant_accel_note = lambda *_args, **_kwargs: ""
|
module.quant_accel_note = lambda *_args, **_kwargs: ""
|
||||||
module.lora_active = lambda _model: False
|
module.lora_active = lambda _model: False
|
||||||
module.resolve_shot_frames = lambda *_args, **_kwargs: (73, "")
|
module.resolve_shot_frames = lambda *_args, **_kwargs: (73, "")
|
||||||
module.plan_beat_frames = lambda beats, fps, budget, per_beat=True: ([73] * len(beats), [])
|
module.plan_beat_frames = lambda beats, fps, budget: ([73] * len(beats), [])
|
||||||
module.dialogue_fit_warnings = lambda *_args, **_kwargs: []
|
module.dialogue_fit_warnings = lambda *_args, **_kwargs: []
|
||||||
module.dialogue_filler_warnings = lambda *_args, **_kwargs: []
|
module.dialogue_filler_warnings = lambda *_args, **_kwargs: []
|
||||||
module.distribute_generations = lambda anchor, beats, *_args, **_kwargs: [
|
module.distribute_generations = lambda anchor, beats, *_args, **_kwargs: [
|
||||||
@@ -782,9 +1192,9 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
plan_only=True,
|
plan_only=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertIn("# anatomy_guard: injected on shot(s) 2", result[3])
|
self.assertIn("Prompt 1", result[3])
|
||||||
self.assertIn("# shot 1 refs: none", result[3])
|
self.assertIn("Beat 1 info\nAnatomy guard: not injected for this prompt\nReferences used: none", result[3])
|
||||||
self.assertIn("# shot 2 refs: none", result[3])
|
self.assertIn("Beat 2 info\nAnatomy guard: injected into this prompt\nReferences used: none", result[3])
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"[Generation 1] editorial room, soft practical lighting. Francine stands alone.",
|
"[Generation 1] editorial room, soft practical lighting. Francine stands alone.",
|
||||||
result[3],
|
result[3],
|
||||||
|
|||||||
@@ -245,7 +245,6 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
|
|
||||||
result = node.build_reference(
|
result = node.build_reference(
|
||||||
image=image,
|
image=image,
|
||||||
picture_id="2",
|
|
||||||
character_id="char_dave",
|
character_id="char_dave",
|
||||||
name="Dave",
|
name="Dave",
|
||||||
alias="The Locksmith",
|
alias="The Locksmith",
|
||||||
@@ -270,10 +269,10 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
"id": "char-dave",
|
"id": "char-dave",
|
||||||
"name": "Dave",
|
"name": "Dave",
|
||||||
"aliases": ["The Locksmith"],
|
"aliases": ["The Locksmith"],
|
||||||
"picture_id": 2,
|
"picture_id": None,
|
||||||
"picture_label": "<Picture 2>",
|
"picture_label": "",
|
||||||
"image": image,
|
"image": image,
|
||||||
"summary": "Dave shown in <Picture 2>.",
|
"summary": "Dave reference.",
|
||||||
"description": "Square jaw, tired eyes, cropped brown hair.",
|
"description": "Square jaw, tired eyes, cropped brown hair.",
|
||||||
"wardrobe": "weathered red flight jacket, grey cargo shorts, black boots",
|
"wardrobe": "weathered red flight jacket, grey cargo shorts, black boots",
|
||||||
"general": "wears a long grey coat",
|
"general": "wears a long grey coat",
|
||||||
@@ -289,13 +288,16 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_character_reference_input_types_do_not_expose_picture_id(self):
|
||||||
|
required = self.image_nodes.DumasCharacterReferenceNode.INPUT_TYPES()["required"]
|
||||||
|
self.assertNotIn("picture_id", required)
|
||||||
|
|
||||||
def test_character_reference_handles_missing_optional_fields(self):
|
def test_character_reference_handles_missing_optional_fields(self):
|
||||||
node = self.image_nodes.DumasCharacterReferenceNode()
|
node = self.image_nodes.DumasCharacterReferenceNode()
|
||||||
image = FakeTensorBatch()
|
image = FakeTensorBatch()
|
||||||
|
|
||||||
result = node.build_reference(
|
result = node.build_reference(
|
||||||
image=image,
|
image=image,
|
||||||
picture_id="4",
|
|
||||||
character_id="",
|
character_id="",
|
||||||
name="",
|
name="",
|
||||||
alias="",
|
alias="",
|
||||||
@@ -313,8 +315,8 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
|
|
||||||
reference = result[0]
|
reference = result[0]
|
||||||
self.assertEqual(reference["kind"], "character")
|
self.assertEqual(reference["kind"], "character")
|
||||||
self.assertEqual(reference["picture_id"], 4)
|
self.assertIsNone(reference["picture_id"])
|
||||||
self.assertEqual(reference["picture_label"], "<Picture 4>")
|
self.assertEqual(reference["picture_label"], "")
|
||||||
self.assertEqual(reference["wardrobe"], "")
|
self.assertEqual(reference["wardrobe"], "")
|
||||||
self.assertEqual(reference["general"], "")
|
self.assertEqual(reference["general"], "")
|
||||||
self.assertEqual(reference["facts"]["age"], "")
|
self.assertEqual(reference["facts"]["age"], "")
|
||||||
@@ -325,7 +327,6 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
|
|
||||||
result = node.build_reference(
|
result = node.build_reference(
|
||||||
image=image,
|
image=image,
|
||||||
picture_id="9",
|
|
||||||
location_id="coffee-shop-01",
|
location_id="coffee-shop-01",
|
||||||
name="Coffee Shop",
|
name="Coffee Shop",
|
||||||
alias="Cafe Interior",
|
alias="Cafe Interior",
|
||||||
@@ -340,10 +341,10 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
"id": "coffee-shop-01",
|
"id": "coffee-shop-01",
|
||||||
"name": "Coffee Shop",
|
"name": "Coffee Shop",
|
||||||
"aliases": ["Cafe Interior"],
|
"aliases": ["Cafe Interior"],
|
||||||
"picture_id": 9,
|
"picture_id": None,
|
||||||
"picture_label": "<Picture 9>",
|
"picture_label": "",
|
||||||
"image": image,
|
"image": image,
|
||||||
"summary": "Coffee Shop shown in <Picture 9>.",
|
"summary": "Coffee Shop reference.",
|
||||||
"description": "Warm tungsten lighting, narrow counter, rainy front window.",
|
"description": "Warm tungsten lighting, narrow counter, rainy front window.",
|
||||||
"wardrobe": "",
|
"wardrobe": "",
|
||||||
"general": "Evening ambience, cramped but cozy.",
|
"general": "Evening ambience, cramped but cozy.",
|
||||||
@@ -351,6 +352,45 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_location_reference_input_types_do_not_expose_picture_id(self):
|
||||||
|
required = self.image_nodes.DumasLocationReferenceNode.INPUT_TYPES()["required"]
|
||||||
|
self.assertNotIn("picture_id", required)
|
||||||
|
|
||||||
|
def test_normalize_reference_upgrades_generic_summary_with_socket_picture_id(self):
|
||||||
|
image = FakeTensorBatch()
|
||||||
|
|
||||||
|
reference = self.image_nodes.normalize_reference(
|
||||||
|
{
|
||||||
|
"kind": "character",
|
||||||
|
"name": "Dave",
|
||||||
|
"image": image,
|
||||||
|
"summary": "Dave reference.",
|
||||||
|
},
|
||||||
|
picture_id=3,
|
||||||
|
allow_image_fallback=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(reference["picture_id"], 3)
|
||||||
|
self.assertEqual(reference["picture_label"], "<Picture 3>")
|
||||||
|
self.assertEqual(reference["summary"], "Dave shown in <Picture 3>.")
|
||||||
|
|
||||||
|
def test_normalize_reference_keeps_custom_summary_when_socket_picture_id_is_added(self):
|
||||||
|
image = FakeTensorBatch()
|
||||||
|
|
||||||
|
reference = self.image_nodes.normalize_reference(
|
||||||
|
{
|
||||||
|
"kind": "character",
|
||||||
|
"name": "Dave",
|
||||||
|
"image": image,
|
||||||
|
"summary": "Primary hero look for the opening close-up.",
|
||||||
|
},
|
||||||
|
picture_id=3,
|
||||||
|
allow_image_fallback=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(reference["picture_id"], 3)
|
||||||
|
self.assertEqual(reference["summary"], "Primary hero look for the opening close-up.")
|
||||||
|
|
||||||
def test_anchor_style_node_exposes_requested_presets(self):
|
def test_anchor_style_node_exposes_requested_presets(self):
|
||||||
input_types = self.image_nodes.DumasAnchorStyleNode.INPUT_TYPES()
|
input_types = self.image_nodes.DumasAnchorStyleNode.INPUT_TYPES()
|
||||||
options = input_types["required"]["anchor_style"][0]
|
options = input_types["required"]["anchor_style"][0]
|
||||||
@@ -375,7 +415,7 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertIn("found-footage", result[0])
|
self.assertIn("found-footage", result[0])
|
||||||
self.assertIn("real time", result[0])
|
self.assertIn("real time", result[0])
|
||||||
self.assertIn("persistent camera language", result[0])
|
self.assertNotIn("persistent camera language", result[0])
|
||||||
|
|
||||||
def test_anchor_style_node_prefers_manual_description_edits(self):
|
def test_anchor_style_node_prefers_manual_description_edits(self):
|
||||||
node = self.image_nodes.DumasAnchorStyleNode()
|
node = self.image_nodes.DumasAnchorStyleNode()
|
||||||
|
|||||||
Reference in New Issue
Block a user