Implement structured H3 reference objects
This commit is contained in:
@@ -34,13 +34,14 @@
|
|||||||
- Reads back the nine optional images for a selected MiniMax H3 plan scene, for example by connecting the current `clip_index`.
|
- Reads back the nine optional images for a selected MiniMax H3 plan scene, for example by connecting the current `clip_index`.
|
||||||
|
|
||||||
- `Dumas H3 Long Videos (FL2VA + REF2VA)`
|
- `Dumas H3 Long Videos (FL2VA + REF2VA)`
|
||||||
- Inputs: H3 model stack, prompt socket, optional `first_frame`, optional `ref_image_1`..`ref_image_9`, optional `plan`, optional `plan_scene_index`, plus the upstream long-video control surface for pacing, continuity, audio, overlays, and guards
|
- Inputs: H3 model stack, prompt socket, optional `first_frame`, optional `ref_1`..`ref_9`, optional `plan`, optional `plan_scene_index`, plus the upstream long-video control surface for pacing, continuity, audio, overlays, and guards
|
||||||
- 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.
|
||||||
- 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_image_2` and `ref_image_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.
|
||||||
- A connected H3 plan can now supply the current scene’s 9-image bundle directly; any directly-wired `ref_image_*` socket overrides the same numbered plan slot.
|
- Character refs now contribute appearance and wardrobe context from the same structured object, while location refs contribute environment context from theirs.
|
||||||
|
- A connected H3 plan can now supply the current scene’s 9-image bundle directly; any directly-wired `ref_*` socket overrides the same numbered plan slot.
|
||||||
- The default ref2v bias is now stronger: `ref_mode` defaults to `auto ref2v` so untagged prompts condition every shot instead of only shot 1, and `ref_noise_aug` defaults to `0.95` rather than the upstream-literal `0.999`.
|
- The default ref2v bias is now stronger: `ref_mode` defaults to `auto ref2v` so untagged prompts condition every shot instead of only shot 1, and `ref_noise_aug` defaults to `0.95` rather than the upstream-literal `0.999`.
|
||||||
|
|
||||||
- `Dumas H3 Shot Length`
|
- `Dumas H3 Shot Length`
|
||||||
@@ -53,11 +54,15 @@
|
|||||||
- Outputs: `format`, `report`
|
- Outputs: `format`, `report`
|
||||||
- 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 Helper`
|
- `Dumas Character Reference`
|
||||||
- Inputs: `image1`, `image2`, `image1_picture_id`, `image2_picture_id`, `character_id`, `name`, `alias`, `gender`, `age`, `nationality`, `occupation`, `height_feet`, `height_inches`, `accent`, `general`
|
- Inputs: `image`, `picture_id`, `character_id`, `name`, `alias`, `gender`, `age`, `nationality`, `occupation`, `height_feet`, `height_inches`, `accent`, `description`, `general`, `wardrobe`
|
||||||
- Outputs: `image1`, `image2`, `character_text`
|
- Output: `reference`
|
||||||
- Passes both images through unchanged and builds a character reference string such as `<Picture 2> and <Picture 3> reference the same character who is called Dave.`
|
- Builds one structured `REFERENCE` object carrying the conditioning image, identity description, wardrobe, general notes, and simple facts together.
|
||||||
- Collapses optional non-visual facts such as alias, gender, age, nationality, occupation, height, and accent into one comma-separated sentence, then ends with the freeform note as the final sentence when provided.
|
|
||||||
|
- `Dumas Location Reference`
|
||||||
|
- Inputs: `image`, `picture_id`, `location_id`, `name`, `alias`, `description`, `general`
|
||||||
|
- 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.
|
||||||
|
|
||||||
- `Dumas JSON String to Object`
|
- `Dumas JSON String to Object`
|
||||||
- Input: `json_string`
|
- Input: `json_string`
|
||||||
@@ -215,7 +220,7 @@ decr -> use index - 1
|
|||||||
|
|
||||||
`Dumas H3 Plan Attach Scene Images` and `Dumas H3 Plan Extract Scene Images` are a companion pair for `ComfyUI-MiniMaxH3-Contex-Loop` and the local `ref2v` lane. The upstream H3 plan node cannot dynamically grow nine new image sockets for every JSON-defined scene, so Dumas stores scene image bindings beside the plan using a lightweight token and an in-memory registry. That keeps `plan.json` archiving intact while still letting you wire up nine IMAGE sockets per scene through chained helper nodes.
|
`Dumas H3 Plan Attach Scene Images` and `Dumas H3 Plan Extract Scene Images` are a companion pair for `ComfyUI-MiniMaxH3-Contex-Loop` and the local `ref2v` lane. The upstream H3 plan node cannot dynamically grow nine new image sockets for every JSON-defined scene, so Dumas stores scene image bindings beside the plan using a lightweight token and an in-memory registry. That keeps `plan.json` archiving intact while still letting you wire up nine IMAGE sockets per scene through chained helper nodes.
|
||||||
|
|
||||||
`Dumas Character Helper` lives in `Dumas/String`. Use the picture ID dropdowns to decide which `<Picture N>` tags get mentioned in the generated text, while the two IMAGE sockets continue downstream unchanged. The node is tuned for useful non-visible facts rather than visual descriptions already obvious from the reference images.
|
`Dumas Character Reference` and `Dumas Location Reference` live in `Dumas/MiniMax`. Both output a structured `REFERENCE` object that carries the image plus its semantic payload. `Dumas H3 Long Videos` accepts those `REFERENCE` sockets directly on `ref_1`..`ref_9`, resolves `<Picture N>` against the wired slot positions, and can also pull character wardrobe context from the structured ref data when `character_memory` is left blank.
|
||||||
|
|
||||||
`Dumas Strip Iteration Suffix` keeps the part before the first underscore and drops the rest. Names like `char123_pose_final.png` become `char123.png`, while names with no underscore such as `char123.png` are left untouched.
|
`Dumas Strip Iteration Suffix` keeps the part before the first underscore and drops the rest. Names like `char123_pose_final.png` become `char123.png`, while names with no underscore such as `char123.png` are left untouched.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,386 @@
|
|||||||
|
# Dumas Reference System Spec
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Replace the current image-only / text-fragment character-reference flow with one
|
||||||
|
structured reference system that works for both:
|
||||||
|
|
||||||
|
- characters
|
||||||
|
- locations / environments
|
||||||
|
|
||||||
|
Compatibility with old graphs is **not** a goal. This is a clean redesign for a
|
||||||
|
single-user workflow.
|
||||||
|
|
||||||
|
## Core Direction
|
||||||
|
|
||||||
|
The system should stop treating a reference as "just an image".
|
||||||
|
|
||||||
|
A reference should be one structured object that carries:
|
||||||
|
|
||||||
|
- the image used for model conditioning
|
||||||
|
- the semantic identity of the reference
|
||||||
|
- the descriptive text tied to that reference
|
||||||
|
- the routing metadata needed by H3 beat / prompt logic
|
||||||
|
|
||||||
|
This object type is called `REFERENCE`.
|
||||||
|
|
||||||
|
## Main Outcomes
|
||||||
|
|
||||||
|
After this redesign:
|
||||||
|
|
||||||
|
- H3 ref sockets accept `REFERENCE`, not raw `IMAGE`
|
||||||
|
- `<Picture N>` resolves to the bound `REFERENCE` object
|
||||||
|
- character references can provide identity, description, and wardrobe from one source
|
||||||
|
- location references can provide environment description from one source
|
||||||
|
- beat prompt logic no longer has to guess where a name or picture tag should pull
|
||||||
|
its descriptive payload from
|
||||||
|
|
||||||
|
## Reference Kinds
|
||||||
|
|
||||||
|
`REFERENCE` is one base type with a `kind` field.
|
||||||
|
|
||||||
|
Initial supported kinds:
|
||||||
|
|
||||||
|
- `character`
|
||||||
|
- `location`
|
||||||
|
|
||||||
|
Future kinds could be added later if useful, but they are out of scope for the
|
||||||
|
first pass.
|
||||||
|
|
||||||
|
## Reference Object Schema
|
||||||
|
|
||||||
|
The `REFERENCE` object should be a plain JSON-serializable structure plus an
|
||||||
|
attached Comfy image payload where needed by downstream nodes.
|
||||||
|
|
||||||
|
Conceptual schema:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": "character",
|
||||||
|
"id": "francine",
|
||||||
|
"name": "Francine",
|
||||||
|
"aliases": [],
|
||||||
|
"picture_id": 1,
|
||||||
|
"picture_label": "<Picture 1>",
|
||||||
|
"image": "<IMAGE>",
|
||||||
|
"summary": "Same character shown in <Picture 1>.",
|
||||||
|
"description": "Face, hair, proportions, persistent visual identity.",
|
||||||
|
"wardrobe": "Clothing / styling / persistent look notes.",
|
||||||
|
"general": "Freeform notes.",
|
||||||
|
"facts": {
|
||||||
|
"gender": "",
|
||||||
|
"age": "",
|
||||||
|
"nationality": "",
|
||||||
|
"occupation": "",
|
||||||
|
"height_feet": "",
|
||||||
|
"height_inches": "",
|
||||||
|
"accent": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For locations:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": "location",
|
||||||
|
"id": "coffee-shop-01",
|
||||||
|
"name": "Coffee Shop",
|
||||||
|
"aliases": [],
|
||||||
|
"picture_id": 2,
|
||||||
|
"picture_label": "<Picture 2>",
|
||||||
|
"image": "<IMAGE>",
|
||||||
|
"summary": "Environment shown in <Picture 2>.",
|
||||||
|
"description": "Persistent environment / layout / atmosphere description.",
|
||||||
|
"wardrobe": "",
|
||||||
|
"general": "Freeform notes.",
|
||||||
|
"facts": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Field Meaning
|
||||||
|
|
||||||
|
### Shared fields
|
||||||
|
|
||||||
|
- `kind`
|
||||||
|
- Required.
|
||||||
|
- `character` or `location`.
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- Required.
|
||||||
|
- Stable, slug-like internal identifier.
|
||||||
|
- Used for matching and future persistence.
|
||||||
|
|
||||||
|
- `name`
|
||||||
|
- Optional but strongly recommended.
|
||||||
|
- Human-readable label.
|
||||||
|
|
||||||
|
- `aliases`
|
||||||
|
- Optional list of alternate match names.
|
||||||
|
|
||||||
|
- `picture_id`
|
||||||
|
- Optional integer representing the intended `<Picture N>` identity.
|
||||||
|
- This is authoring metadata, not the final socket position.
|
||||||
|
|
||||||
|
- `picture_label`
|
||||||
|
- Derived convenience text like `<Picture 1>`.
|
||||||
|
|
||||||
|
- `image`
|
||||||
|
- Required.
|
||||||
|
- The actual Comfy `IMAGE` payload used for reference conditioning.
|
||||||
|
|
||||||
|
- `summary`
|
||||||
|
- Short purpose statement for prompt assembly and debugging.
|
||||||
|
|
||||||
|
- `description`
|
||||||
|
- Primary descriptive payload tied to the reference.
|
||||||
|
- For characters this is the physical identity description.
|
||||||
|
- For locations this is the environment/layout/atmosphere description.
|
||||||
|
|
||||||
|
- `general`
|
||||||
|
- Optional freeform notes.
|
||||||
|
|
||||||
|
- `facts`
|
||||||
|
- Optional structured detail map.
|
||||||
|
|
||||||
|
### Character-only field
|
||||||
|
|
||||||
|
- `wardrobe`
|
||||||
|
- Optional but expected for character references.
|
||||||
|
- Persistent clothing / styling / accessories / visual state notes.
|
||||||
|
|
||||||
|
### Location usage note
|
||||||
|
|
||||||
|
For `kind = "location"`, `wardrobe` should be empty.
|
||||||
|
|
||||||
|
## Producer Nodes
|
||||||
|
|
||||||
|
### 1. Character helper replacement
|
||||||
|
|
||||||
|
Current `Dumas Character Helper` should be redesigned into a character reference
|
||||||
|
builder.
|
||||||
|
|
||||||
|
Recommended name:
|
||||||
|
|
||||||
|
- `Dumas Character Reference`
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
|
||||||
|
- `image`
|
||||||
|
- `picture_id`
|
||||||
|
- `character_id`
|
||||||
|
- `name`
|
||||||
|
- `alias`
|
||||||
|
- `gender`
|
||||||
|
- `age`
|
||||||
|
- `nationality`
|
||||||
|
- `occupation`
|
||||||
|
- `height_feet`
|
||||||
|
- `height_inches`
|
||||||
|
- `accent`
|
||||||
|
- `description`
|
||||||
|
- `wardrobe`
|
||||||
|
- `general`
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
|
||||||
|
- `reference` (`REFERENCE`)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- This node should stop emitting the current mixed output pattern.
|
||||||
|
- It should build one clean `REFERENCE` object with `kind = "character"`.
|
||||||
|
|
||||||
|
### 2. Location / environment helper
|
||||||
|
|
||||||
|
Add a new builder node.
|
||||||
|
|
||||||
|
Recommended name:
|
||||||
|
|
||||||
|
- `Dumas Location Reference`
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
|
||||||
|
- `image`
|
||||||
|
- `picture_id`
|
||||||
|
- `location_id`
|
||||||
|
- `name`
|
||||||
|
- `alias`
|
||||||
|
- `description`
|
||||||
|
- `general`
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
|
||||||
|
- `reference` (`REFERENCE`)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- This node builds one `REFERENCE` object with `kind = "location"`.
|
||||||
|
- It should not attempt to mimic character-specific fields.
|
||||||
|
|
||||||
|
## Consumer Changes
|
||||||
|
|
||||||
|
## H3 Long Videos
|
||||||
|
|
||||||
|
Current numbered sockets:
|
||||||
|
|
||||||
|
- `ref_image_1` .. `ref_image_9`
|
||||||
|
|
||||||
|
should become:
|
||||||
|
|
||||||
|
- `ref_1` .. `ref_9`
|
||||||
|
|
||||||
|
Type:
|
||||||
|
|
||||||
|
- `REFERENCE`
|
||||||
|
|
||||||
|
Each socket should provide access to:
|
||||||
|
|
||||||
|
- the underlying `image`
|
||||||
|
- the structured metadata
|
||||||
|
|
||||||
|
### Internal handling
|
||||||
|
|
||||||
|
The H3 node should internally derive two parallel lanes:
|
||||||
|
|
||||||
|
1. reference images for model conditioning
|
||||||
|
2. reference definitions for semantic beat / prompt resolution
|
||||||
|
|
||||||
|
It must no longer depend on detached text sources to know what a reference means.
|
||||||
|
|
||||||
|
## Beat / prompt resolution rules
|
||||||
|
|
||||||
|
### Resolution by `<Picture N>`
|
||||||
|
|
||||||
|
If a beat contains `<Picture N>`:
|
||||||
|
|
||||||
|
- map `N` to the bound socket position in that shot
|
||||||
|
- resolve the bound `REFERENCE`
|
||||||
|
- use the resolved object's data when generating the shot-level descriptive payload
|
||||||
|
|
||||||
|
### Resolution by character name
|
||||||
|
|
||||||
|
For `kind = "character"`:
|
||||||
|
|
||||||
|
- match against `name`
|
||||||
|
- match against `aliases`
|
||||||
|
- optionally match `id`
|
||||||
|
|
||||||
|
When a character is matched:
|
||||||
|
|
||||||
|
- pull identity text from `description`
|
||||||
|
- pull clothing / styling from `wardrobe`
|
||||||
|
- pull additional context from `general` and `facts` if needed
|
||||||
|
|
||||||
|
### Resolution by location
|
||||||
|
|
||||||
|
For `kind = "location"`:
|
||||||
|
|
||||||
|
- `<Picture N>` should resolve directly
|
||||||
|
- later name-based location matching can be added if useful, but the first pass
|
||||||
|
does not need aggressive free-text environment matching
|
||||||
|
|
||||||
|
When a location is matched:
|
||||||
|
|
||||||
|
- pull environment text from `description`
|
||||||
|
- optionally use `general`
|
||||||
|
|
||||||
|
## Prompt Assembly Expectations
|
||||||
|
|
||||||
|
Character refs should contribute:
|
||||||
|
|
||||||
|
- who the subject is
|
||||||
|
- what persistent visual identity should be preserved
|
||||||
|
- what clothing / styling should persist
|
||||||
|
|
||||||
|
Location refs should contribute:
|
||||||
|
|
||||||
|
- where the scene is
|
||||||
|
- what environment layout / mood / persistent scenic identity should be preserved
|
||||||
|
|
||||||
|
The node should keep these conceptually separate.
|
||||||
|
|
||||||
|
Character reference text should not be mistaken for environment text.
|
||||||
|
Location reference text should not be mistaken for wardrobe or identity text.
|
||||||
|
|
||||||
|
## Multi-reference Behavior
|
||||||
|
|
||||||
|
The system must support multiple references bound at once.
|
||||||
|
|
||||||
|
Typical cases:
|
||||||
|
|
||||||
|
- one character + one location
|
||||||
|
- multiple character references
|
||||||
|
- one character sheet reused across many beats
|
||||||
|
|
||||||
|
The H3 node should preserve current numbered socket behavior for explicit routing,
|
||||||
|
but with `REFERENCE` objects replacing plain images.
|
||||||
|
|
||||||
|
## Plan / scene integration
|
||||||
|
|
||||||
|
The current H3 plan scene image system should evolve in parallel.
|
||||||
|
|
||||||
|
Where plan scenes currently carry image bundles, they should eventually carry
|
||||||
|
reference bundles instead.
|
||||||
|
|
||||||
|
This is not required for the first code pass if it would enlarge the change too
|
||||||
|
much, but the implementation should avoid boxing us into image-only assumptions.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
Not required for first pass:
|
||||||
|
|
||||||
|
- backward compatibility with old image-only graphs
|
||||||
|
- migration helpers
|
||||||
|
- automatic graph conversion
|
||||||
|
- advanced location name matching
|
||||||
|
- persistent storage outside the graph object itself
|
||||||
|
|
||||||
|
## Recommended Implementation Order
|
||||||
|
|
||||||
|
1. Define the `REFERENCE` type contract in code.
|
||||||
|
2. Replace `Dumas Character Helper` with a character-reference builder node.
|
||||||
|
3. Add `Dumas Location Reference`.
|
||||||
|
4. Update H3 long videos sockets from `IMAGE` to `REFERENCE`.
|
||||||
|
5. Update internal ref collection logic to extract both image and metadata.
|
||||||
|
6. Update beat / prompt resolution to use `REFERENCE` objects.
|
||||||
|
7. Update tests.
|
||||||
|
8. Update README and workflow docs.
|
||||||
|
|
||||||
|
## Testing Requirements
|
||||||
|
|
||||||
|
Add tests for:
|
||||||
|
|
||||||
|
- character reference object construction
|
||||||
|
- location reference object construction
|
||||||
|
- `<Picture N>` resolving to the correct `REFERENCE`
|
||||||
|
- name-based character lookup resolving to the correct `REFERENCE`
|
||||||
|
- wardrobe text being pulled only from character refs
|
||||||
|
- location description being pulled only from location refs
|
||||||
|
- mixed character + location ref usage in one prompt
|
||||||
|
- sparse numbered sockets still resolving correctly
|
||||||
|
|
||||||
|
## Open Design Choices
|
||||||
|
|
||||||
|
These should be decided during implementation, but the default recommendation is:
|
||||||
|
|
||||||
|
- one `REFERENCE` output per helper node
|
||||||
|
- numbered H3 sockets remain explicit
|
||||||
|
- no separate cast object in first pass
|
||||||
|
- no compatibility fallback path
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This redesign turns references into first-class structured inputs.
|
||||||
|
|
||||||
|
The key rule is:
|
||||||
|
|
||||||
|
> A reference socket should carry both the image and the meaning of that image.
|
||||||
|
|
||||||
|
That is the change that removes the current ambiguity between:
|
||||||
|
|
||||||
|
- image conditioning
|
||||||
|
- character identity
|
||||||
|
- wardrobe data
|
||||||
|
- location / environment description
|
||||||
|
|
||||||
+171
-24
@@ -8,7 +8,7 @@ One node covering both of H3's conditioning tasks:
|
|||||||
* REF2VA -- reference images condition the shot on what a character LOOKS like,
|
* REF2VA -- reference images condition the shot on what a character LOOKS like,
|
||||||
independent of any frame.
|
independent of any frame.
|
||||||
|
|
||||||
Connect nothing to ref_image_* and it behaves exactly as the FL2VA node always
|
Connect nothing to ref_* and it behaves exactly as the FL2VA node always
|
||||||
did. Connect a reference and `ref_mode` decides which shots use it.
|
did. Connect a reference and `ref_mode` decides which shots use it.
|
||||||
|
|
||||||
THE ONE RULE: a shot carries EITHER references or the last-frame handoff, never
|
THE ONE RULE: a shot carries EITHER references or the last-frame handoff, never
|
||||||
@@ -275,7 +275,7 @@ ADDED_WIDGETS = (
|
|||||||
"exposed_terms", "anatomy_guard", "lock_restraints", "solidity_guard",
|
"exposed_terms", "anatomy_guard", "lock_restraints", "solidity_guard",
|
||||||
"motion_guard", "contact_guard",
|
"motion_guard", "contact_guard",
|
||||||
"auto_soundscape", "allow_nonspeech_vocals",
|
"auto_soundscape", "allow_nonspeech_vocals",
|
||||||
"ref_image_5", "ref_image_6", "ref_image_7", "ref_image_8", "ref_image_9",
|
"ref_5", "ref_6", "ref_7", "ref_8", "ref_9",
|
||||||
"plan", "plan_scene_index",
|
"plan", "plan_scene_index",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3869,13 +3869,14 @@ def ref_image_canvas(w, h, gen_w, gen_h, mode="match"):
|
|||||||
|
|
||||||
|
|
||||||
def _build_ref_images(vae, images, gen_w, gen_h, mode="match"):
|
def _build_ref_images(vae, images, gen_w, gen_h, mode="match"):
|
||||||
"""(tokenizer items, DiT blocks) for a list of reference IMAGE tensors.
|
"""(tokenizer items, DiT blocks) for a list of reference images.
|
||||||
|
|
||||||
The tokenizer labels each one `<Picture N>:` itself, in the order given here --
|
The tokenizer labels each one `<Picture N>:` itself, in the order given here --
|
||||||
so the roster the prompt refers to is decided by input order, not by anything
|
so the roster the prompt refers to is decided by input order, not by anything
|
||||||
written in the prompt."""
|
written in the prompt."""
|
||||||
items, blocks = [], []
|
items, blocks = [], []
|
||||||
for img in images:
|
for source in images:
|
||||||
|
img = _reference_image(source)
|
||||||
if img is None:
|
if img is None:
|
||||||
continue
|
continue
|
||||||
h, w = int(img.shape[1]), int(img.shape[2])
|
h, w = int(img.shape[1]), int(img.shape[2])
|
||||||
@@ -3891,7 +3892,7 @@ def _build_shot_conditioning(clip, vae, prompt, width, height, length, fps, hand
|
|||||||
ref_images=None, ref_image_size="match", ref_noise_aug=None,
|
ref_images=None, ref_image_size="match", ref_noise_aug=None,
|
||||||
audio_vae=None, silent=False):
|
audio_vae=None, silent=False):
|
||||||
latent, fc = _empty_av_latent(width, height, length, fps)
|
latent, fc = _empty_av_latent(width, height, length, fps)
|
||||||
refs = [r for r in (ref_images or []) if r is not None]
|
refs = [r for r in (ref_images or []) if _reference_image(r) is not None]
|
||||||
if refs:
|
if refs:
|
||||||
# ref2va: this shot is reference-conditioned rather than keyframe-conditioned,
|
# ref2va: this shot is reference-conditioned rather than keyframe-conditioned,
|
||||||
# and run() decides which per shot. A tagged shot is handed the previous
|
# and run() decides which per shot. A tagged shot is handed the previous
|
||||||
@@ -4044,9 +4045,135 @@ def picture_tags(text):
|
|||||||
return sorted({int(m.group(1)) for m in _PICTURE_TAG.finditer(text or "")})
|
return sorted({int(m.group(1)) for m in _PICTURE_TAG.finditer(text or "")})
|
||||||
|
|
||||||
|
|
||||||
|
def _reference_slot(ref, slot_index=None):
|
||||||
|
return _image_nodes.normalize_reference(ref, picture_id=slot_index, allow_image_fallback=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _reference_image(ref):
|
||||||
|
try:
|
||||||
|
normalized = _reference_slot(ref)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return normalized.get("image")
|
||||||
|
|
||||||
|
|
||||||
|
def _reference_text(value):
|
||||||
|
return " ".join(str(value or "").split()).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _reference_sentence(value):
|
||||||
|
text = _reference_text(value)
|
||||||
|
if text and text[-1] not in ".!?":
|
||||||
|
text += "."
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _reference_name_keys(ref):
|
||||||
|
names = []
|
||||||
|
for key in ("name", "id"):
|
||||||
|
value = _reference_text(ref.get(key))
|
||||||
|
if value:
|
||||||
|
names.append(value)
|
||||||
|
for alias in ref.get("aliases") or []:
|
||||||
|
value = _reference_text(alias)
|
||||||
|
if value:
|
||||||
|
names.append(value)
|
||||||
|
seen = set()
|
||||||
|
out = []
|
||||||
|
for name in names:
|
||||||
|
key = name.lower()
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
out.append(name)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _slot_refs_for_text(text, ref_slots):
|
||||||
|
refs = []
|
||||||
|
for slot_number in picture_tags(text):
|
||||||
|
if not (1 <= slot_number <= len(ref_slots or [])):
|
||||||
|
continue
|
||||||
|
ref = ref_slots[slot_number - 1]
|
||||||
|
if _reference_image(ref) is None:
|
||||||
|
continue
|
||||||
|
refs.append((slot_number, _reference_slot(ref, slot_number)))
|
||||||
|
return refs
|
||||||
|
|
||||||
|
|
||||||
|
def _named_character_refs_for_text(text, ref_slots):
|
||||||
|
haystack = str(text or "")
|
||||||
|
matched = []
|
||||||
|
for slot_number, raw in enumerate(ref_slots or [], 1):
|
||||||
|
ref = _reference_slot(raw, slot_number)
|
||||||
|
if ref.get("kind") != "character" or _reference_image(ref) is None:
|
||||||
|
continue
|
||||||
|
for name in _reference_name_keys(ref):
|
||||||
|
if re.search(r"\b" + re.escape(name) + r"\b", haystack, re.I):
|
||||||
|
matched.append((slot_number, ref))
|
||||||
|
break
|
||||||
|
return matched
|
||||||
|
|
||||||
|
|
||||||
|
def _matched_reference_slots(text, ref_slots):
|
||||||
|
matched = []
|
||||||
|
seen = set()
|
||||||
|
for slot_number, ref in _slot_refs_for_text(text, ref_slots) + _named_character_refs_for_text(text, ref_slots):
|
||||||
|
if slot_number in seen:
|
||||||
|
continue
|
||||||
|
seen.add(slot_number)
|
||||||
|
matched.append((slot_number, ref))
|
||||||
|
return matched
|
||||||
|
|
||||||
|
|
||||||
|
def _reference_context_for_text(text, ref_slots):
|
||||||
|
parts = []
|
||||||
|
for slot_number, ref in _matched_reference_slots(text, ref_slots):
|
||||||
|
label = _reference_text(ref.get("name")) or _reference_text(ref.get("id")) or f"reference {slot_number}"
|
||||||
|
description = _reference_sentence(ref.get("description"))
|
||||||
|
wardrobe = _reference_sentence(ref.get("wardrobe"))
|
||||||
|
general = _reference_sentence(ref.get("general"))
|
||||||
|
if ref.get("kind") == "location":
|
||||||
|
if description:
|
||||||
|
parts.append(f"Location context for {label}: {description}")
|
||||||
|
if general:
|
||||||
|
parts.append(f"Location notes for {label}: {general}")
|
||||||
|
continue
|
||||||
|
if description:
|
||||||
|
parts.append(f"Persistent appearance for {label}: {description}")
|
||||||
|
if wardrobe:
|
||||||
|
parts.append(f"Persistent wardrobe/style for {label}: {wardrobe}")
|
||||||
|
if general:
|
||||||
|
parts.append(f"Character notes for {label}: {general}")
|
||||||
|
return " ".join(parts).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _reference_character_memory(ref_slots):
|
||||||
|
lines = []
|
||||||
|
seen = set()
|
||||||
|
for slot_number, raw in enumerate(ref_slots or [], 1):
|
||||||
|
ref = _reference_slot(raw, slot_number)
|
||||||
|
if ref.get("kind") != "character":
|
||||||
|
continue
|
||||||
|
wardrobe = _reference_text(ref.get("wardrobe"))
|
||||||
|
if not wardrobe:
|
||||||
|
continue
|
||||||
|
label = (
|
||||||
|
_reference_text(ref.get("name"))
|
||||||
|
or (_reference_name_keys(ref)[0] if _reference_name_keys(ref) else "")
|
||||||
|
)
|
||||||
|
line = f"{label} = {wardrobe}" if label else wardrobe
|
||||||
|
key = line.lower()
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
lines.append(line)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def _connected_refs(ref_slots):
|
def _connected_refs(ref_slots):
|
||||||
"""Connected refs only, preserving slot order and skipping empty sockets."""
|
"""Connected refs only, preserving slot order and skipping empty sockets."""
|
||||||
return [ref for ref in (ref_slots or []) if ref is not None]
|
return [ref for ref in (ref_slots or []) if _reference_image(ref) is not None]
|
||||||
|
|
||||||
|
|
||||||
def _plan_scene_refs(plan, scene_index):
|
def _plan_scene_refs(plan, scene_index):
|
||||||
@@ -4054,7 +4181,13 @@ def _plan_scene_refs(plan, scene_index):
|
|||||||
if plan is None:
|
if plan is None:
|
||||||
return (None,) * _image_nodes._H3_PLAN_IMAGE_SLOTS
|
return (None,) * _image_nodes._H3_PLAN_IMAGE_SLOTS
|
||||||
extracted = _image_nodes.DumasH3PlanExtractSceneImagesNode().extract(plan, scene_index)
|
extracted = _image_nodes.DumasH3PlanExtractSceneImagesNode().extract(plan, scene_index)
|
||||||
return tuple(extracted[1:1 + _image_nodes._H3_PLAN_IMAGE_SLOTS])
|
refs = []
|
||||||
|
for slot_number, image in enumerate(
|
||||||
|
extracted[1:1 + _image_nodes._H3_PLAN_IMAGE_SLOTS],
|
||||||
|
1,
|
||||||
|
):
|
||||||
|
refs.append(_reference_slot(image, slot_number) if image is not None else None)
|
||||||
|
return tuple(refs)
|
||||||
|
|
||||||
|
|
||||||
def _merge_ref_slots(direct_slots, plan_slots):
|
def _merge_ref_slots(direct_slots, plan_slots):
|
||||||
@@ -5328,18 +5461,18 @@ class H3LongVideos:
|
|||||||
# the order they are handed that shot.
|
# the order they are handed that shot.
|
||||||
# Refer to socket tags in the prompt if you want a reference bound to
|
# Refer to socket tags in the prompt if you want a reference bound to
|
||||||
# a named character ("Kristy, <Picture 7>, walks in").
|
# a named character ("Kristy, <Picture 7>, walks in").
|
||||||
"ref_image_1": ("IMAGE", {"tooltip": "Reference image <Picture 1> -- identity/appearance "
|
"ref_1": ("REFERENCE", {"tooltip": "Reference object for <Picture 1> -- image plus identity/environment metadata "
|
||||||
"carried into the shots. Which shots receive it is set by ref_mode (or <Picture N> "
|
"carried into the shots. Which shots receive it is set by ref_mode (or <Picture N> "
|
||||||
"tags in the beats); a referenced shot ALSO carries the previous frame as its "
|
"tags in the beats); a referenced shot ALSO carries the previous frame as its "
|
||||||
"keyframe, so taking a reference never costs continuity."}),
|
"keyframe, so taking a reference never costs continuity."}),
|
||||||
"ref_image_2": ("IMAGE", {"tooltip": "Reference image <Picture 2>."}),
|
"ref_2": ("REFERENCE", {"tooltip": "Reference object for <Picture 2>."}),
|
||||||
"ref_image_3": ("IMAGE", {"tooltip": "Reference image <Picture 3>."}),
|
"ref_3": ("REFERENCE", {"tooltip": "Reference object for <Picture 3>."}),
|
||||||
"ref_image_4": ("IMAGE", {"tooltip": "Reference image <Picture 4>."}),
|
"ref_4": ("REFERENCE", {"tooltip": "Reference object for <Picture 4>."}),
|
||||||
"ref_image_5": ("IMAGE", {"tooltip": "Reference image <Picture 5>."}),
|
"ref_5": ("REFERENCE", {"tooltip": "Reference object for <Picture 5>."}),
|
||||||
"ref_image_6": ("IMAGE", {"tooltip": "Reference image <Picture 6>."}),
|
"ref_6": ("REFERENCE", {"tooltip": "Reference object for <Picture 6>."}),
|
||||||
"ref_image_7": ("IMAGE", {"tooltip": "Reference image <Picture 7>."}),
|
"ref_7": ("REFERENCE", {"tooltip": "Reference object for <Picture 7>."}),
|
||||||
"ref_image_8": ("IMAGE", {"tooltip": "Reference image <Picture 8>."}),
|
"ref_8": ("REFERENCE", {"tooltip": "Reference object for <Picture 8>."}),
|
||||||
"ref_image_9": ("IMAGE", {"tooltip": "Reference image <Picture 9>."}),
|
"ref_9": ("REFERENCE", {"tooltip": "Reference object for <Picture 9>."}),
|
||||||
"plan": (
|
"plan": (
|
||||||
"H3_CHAIN_PLAN",
|
"H3_CHAIN_PLAN",
|
||||||
{
|
{
|
||||||
@@ -5356,7 +5489,7 @@ class H3LongVideos:
|
|||||||
"max": 9999,
|
"max": 9999,
|
||||||
"step": 1,
|
"step": 1,
|
||||||
"tooltip": "1-based plan scene index to read from `plan`. "
|
"tooltip": "1-based plan scene index to read from `plan`. "
|
||||||
"Any directly-wired ref_image socket overrides the same slot "
|
"Any directly-wired ref socket overrides the same slot "
|
||||||
"from the plan scene."
|
"from the plan scene."
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -5853,9 +5986,9 @@ class H3LongVideos:
|
|||||||
watermark_opacity=0.75, watermark_margin=3.0,
|
watermark_opacity=0.75, watermark_margin=3.0,
|
||||||
intro_text="", intro_position="center", intro_seconds=3.0, intro_fade=0.6,
|
intro_text="", intro_position="center", intro_seconds=3.0, intro_fade=0.6,
|
||||||
intro_size=9.0, overlay_font="arial.ttf", overlay_stroke=0,
|
intro_size=9.0, overlay_font="arial.ttf", overlay_stroke=0,
|
||||||
ref_image_1=None, ref_image_2=None, ref_image_3=None, ref_image_4=None,
|
ref_1=None, ref_2=None, ref_3=None, ref_4=None,
|
||||||
ref_image_5=None, ref_image_6=None, ref_image_7=None, ref_image_8=None,
|
ref_5=None, ref_6=None, ref_7=None, ref_8=None,
|
||||||
ref_image_9=None,
|
ref_9=None,
|
||||||
plan=None, plan_scene_index=1,
|
plan=None, plan_scene_index=1,
|
||||||
ref_mode="where tagged", ref_image_size="match", ref_noise_aug=0.999,
|
ref_mode="where tagged", ref_image_size="match", ref_noise_aug=0.999,
|
||||||
graph=None, node_id=None):
|
graph=None, node_id=None):
|
||||||
@@ -5878,13 +6011,15 @@ class H3LongVideos:
|
|||||||
fps = H3_FPS
|
fps = H3_FPS
|
||||||
w, h = parse_resolution(resolution)
|
w, h = parse_resolution(resolution)
|
||||||
direct_ref_slots = (
|
direct_ref_slots = (
|
||||||
ref_image_1, ref_image_2, ref_image_3, ref_image_4, ref_image_5,
|
ref_1, ref_2, ref_3, ref_4, ref_5,
|
||||||
ref_image_6, ref_image_7, ref_image_8, ref_image_9,
|
ref_6, ref_7, ref_8, ref_9,
|
||||||
)
|
)
|
||||||
plan_ref_slots = _plan_scene_refs(plan, plan_scene_index)
|
plan_ref_slots = _plan_scene_refs(plan, plan_scene_index)
|
||||||
ref_slots = _merge_ref_slots(direct_ref_slots, plan_ref_slots)
|
ref_slots = _merge_ref_slots(direct_ref_slots, plan_ref_slots)
|
||||||
plan_ref_count = len(_connected_refs(plan_ref_slots))
|
plan_ref_count = len(_connected_refs(plan_ref_slots))
|
||||||
direct_ref_count = len(_connected_refs(direct_ref_slots))
|
direct_ref_count = len(_connected_refs(direct_ref_slots))
|
||||||
|
derived_character_memory = _reference_character_memory(ref_slots)
|
||||||
|
effective_character_memory = (character_memory or "").strip() or derived_character_memory
|
||||||
# A pixel budget overrides the preset's SIZE while keeping its aspect ratio,
|
# A pixel budget overrides the preset's SIZE while keeping its aspect ratio,
|
||||||
# so the dropdown chooses the shape and this chooses how big. Scaling from
|
# so the dropdown chooses the shape and this chooses how big. Scaling from
|
||||||
# the preset's own dimensions is what makes 1.00MP reproduce each native
|
# the preset's own dimensions is what makes 1.00MP reproduce each native
|
||||||
@@ -5933,7 +6068,7 @@ class H3LongVideos:
|
|||||||
# rather than losing a shot and the scene text along with it.
|
# rather than losing a shot and the scene text along with it.
|
||||||
anchor_note = ""
|
anchor_note = ""
|
||||||
if (not anchor_override.strip()) and paras and \
|
if (not anchor_override.strip()) and paras and \
|
||||||
(anchor_contributes_nothing(anchor, character_memory.strip())
|
(anchor_contributes_nothing(anchor, effective_character_memory)
|
||||||
or anchor_is_action_beat(anchor, paras[1:])):
|
or anchor_is_action_beat(anchor, paras[1:])):
|
||||||
preview = " ".join(anchor.split())[:60]
|
preview = " ".join(anchor.split())[:60]
|
||||||
anchor, beat_paras = "", paras
|
anchor, beat_paras = "", paras
|
||||||
@@ -6080,7 +6215,7 @@ class H3LongVideos:
|
|||||||
wardrobe_notes = []
|
wardrobe_notes = []
|
||||||
strip_shots = [] # shots that newly bared a zone -> the NEXT shot starts fresh
|
strip_shots = [] # shots that newly bared a zone -> the NEXT shot starts fresh
|
||||||
gens = distribute_generations(anchor, beats, global_soundscape.strip(),
|
gens = distribute_generations(anchor, beats, global_soundscape.strip(),
|
||||||
non_diegetic_music.strip(), character_memory.strip(),
|
non_diegetic_music.strip(), effective_character_memory,
|
||||||
auto_wardrobe, auto_silence_nonspeech, allow_nonspeech_vocals, count_subjects,
|
auto_wardrobe, auto_silence_nonspeech, allow_nonspeech_vocals, count_subjects,
|
||||||
lora_on, notes_out=wardrobe_notes, auto_props=auto_props,
|
lora_on, notes_out=wardrobe_notes, auto_props=auto_props,
|
||||||
prevent_nudity=prevent_nudity,
|
prevent_nudity=prevent_nudity,
|
||||||
@@ -6092,6 +6227,18 @@ class H3LongVideos:
|
|||||||
motion_guard=motion_guard,
|
motion_guard=motion_guard,
|
||||||
contact_guard=contact_guard,
|
contact_guard=contact_guard,
|
||||||
count_auto=(subject_count_guard == "auto"))
|
count_auto=(subject_count_guard == "auto"))
|
||||||
|
enriched_gens = []
|
||||||
|
for block in gens:
|
||||||
|
context = _reference_context_for_text(block, ref_slots)
|
||||||
|
if context:
|
||||||
|
block = re.sub(
|
||||||
|
r"^(\[Generation \d+\]\s*)",
|
||||||
|
lambda m: m.group(1) + context + " ",
|
||||||
|
block,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
enriched_gens.append(block)
|
||||||
|
gens = enriched_gens
|
||||||
|
|
||||||
# A scenery beat mid-chain hands the next shot a frame with no people in
|
# A scenery beat mid-chain hands the next shot a frame with no people in
|
||||||
# it. Both prompts are individually correct, so this is invisible without
|
# it. Both prompts are individually correct, so this is invisible without
|
||||||
|
|||||||
+265
-53
@@ -20,6 +20,7 @@ _DATE_TOKEN_RE = re.compile(r"%date:([^%]+)%")
|
|||||||
_SERVE_TOKENS = OrderedDict()
|
_SERVE_TOKENS = OrderedDict()
|
||||||
_SERVE_CAP = 256
|
_SERVE_CAP = 256
|
||||||
_H3_PLAN_TYPE = "H3_CHAIN_PLAN"
|
_H3_PLAN_TYPE = "H3_CHAIN_PLAN"
|
||||||
|
_REFERENCE_TYPE = "REFERENCE"
|
||||||
_H3_PLAN_IMAGE_BINDINGS_KEY = "_dumas_scene_image_bindings"
|
_H3_PLAN_IMAGE_BINDINGS_KEY = "_dumas_scene_image_bindings"
|
||||||
_H3_PLAN_IMAGE_BINDINGS = OrderedDict()
|
_H3_PLAN_IMAGE_BINDINGS = OrderedDict()
|
||||||
_H3_PLAN_IMAGE_BINDINGS_CAP = 128
|
_H3_PLAN_IMAGE_BINDINGS_CAP = 128
|
||||||
@@ -454,6 +455,135 @@ def _ensure_sentence(value):
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _slug_like(value):
|
||||||
|
text = _normalize_free_text(value).lower()
|
||||||
|
text = re.sub(r"[^a-z0-9]+", "-", text).strip("-")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_aliases(value):
|
||||||
|
if isinstance(value, (list, tuple)):
|
||||||
|
raw_items = value
|
||||||
|
else:
|
||||||
|
raw_items = re.split(r"[,;\n\r]+", str(value or ""))
|
||||||
|
aliases = []
|
||||||
|
seen = set()
|
||||||
|
for item in raw_items:
|
||||||
|
alias = _normalize_free_text(item)
|
||||||
|
if not alias:
|
||||||
|
continue
|
||||||
|
key = alias.lower()
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
aliases.append(alias)
|
||||||
|
return aliases
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_picture_id(value):
|
||||||
|
try:
|
||||||
|
number = int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
return number if number > 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def _reference_id(explicit_id, name, fallback_prefix):
|
||||||
|
explicit = _slug_like(explicit_id)
|
||||||
|
if explicit:
|
||||||
|
return explicit
|
||||||
|
derived = _slug_like(name)
|
||||||
|
if derived:
|
||||||
|
return derived
|
||||||
|
return f"{fallback_prefix}-{uuid.uuid4().hex[:8]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _reference_label(picture_id):
|
||||||
|
return f"<Picture {picture_id}>" if picture_id else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _reference_summary(kind, name, picture_id):
|
||||||
|
label = _reference_label(picture_id)
|
||||||
|
subject = _normalize_free_text(name) or ("character" if kind == "character" else "location")
|
||||||
|
if label:
|
||||||
|
return f"{subject} shown in {label}."
|
||||||
|
return f"{subject} reference."
|
||||||
|
|
||||||
|
|
||||||
|
def make_reference(
|
||||||
|
*,
|
||||||
|
kind,
|
||||||
|
image,
|
||||||
|
explicit_id="",
|
||||||
|
name="",
|
||||||
|
aliases="",
|
||||||
|
picture_id=None,
|
||||||
|
description="",
|
||||||
|
wardrobe="",
|
||||||
|
general="",
|
||||||
|
facts=None,
|
||||||
|
summary="",
|
||||||
|
):
|
||||||
|
normalized_name = _normalize_free_text(name)
|
||||||
|
normalized_aliases = _parse_aliases(aliases)
|
||||||
|
normalized_picture_id = _coerce_picture_id(picture_id)
|
||||||
|
normalized_kind = "location" if str(kind or "").strip().lower() == "location" else "character"
|
||||||
|
normalized_description = _normalize_free_text(description)
|
||||||
|
normalized_wardrobe = _normalize_free_text(wardrobe)
|
||||||
|
normalized_general = _normalize_free_text(general)
|
||||||
|
normalized_facts = dict(facts or {})
|
||||||
|
normalized_summary = _ensure_sentence(
|
||||||
|
summary or _reference_summary(normalized_kind, normalized_name, normalized_picture_id)
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"kind": normalized_kind,
|
||||||
|
"id": _reference_id(explicit_id, normalized_name, normalized_kind),
|
||||||
|
"name": normalized_name,
|
||||||
|
"aliases": normalized_aliases,
|
||||||
|
"picture_id": normalized_picture_id,
|
||||||
|
"picture_label": _reference_label(normalized_picture_id),
|
||||||
|
"image": image,
|
||||||
|
"summary": normalized_summary,
|
||||||
|
"description": normalized_description,
|
||||||
|
"wardrobe": normalized_wardrobe if normalized_kind == "character" else "",
|
||||||
|
"general": normalized_general,
|
||||||
|
"facts": normalized_facts,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_reference(value, picture_id=None, allow_image_fallback=True):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
reference = dict(value)
|
||||||
|
image = reference.get("image")
|
||||||
|
if image is None and allow_image_fallback:
|
||||||
|
image = value
|
||||||
|
reference["image"] = image
|
||||||
|
if picture_id is not None and not reference.get("picture_id"):
|
||||||
|
reference["picture_id"] = _coerce_picture_id(picture_id)
|
||||||
|
reference["picture_label"] = _reference_label(reference.get("picture_id"))
|
||||||
|
reference.setdefault("kind", "character")
|
||||||
|
reference.setdefault("id", _reference_id("", reference.get("name"), reference["kind"]))
|
||||||
|
reference.setdefault("name", "")
|
||||||
|
reference["aliases"] = _parse_aliases(reference.get("aliases"))
|
||||||
|
reference["summary"] = _ensure_sentence(
|
||||||
|
reference.get("summary")
|
||||||
|
or _reference_summary(reference["kind"], reference.get("name"), reference.get("picture_id"))
|
||||||
|
)
|
||||||
|
reference["description"] = _normalize_free_text(reference.get("description"))
|
||||||
|
reference["wardrobe"] = _normalize_free_text(reference.get("wardrobe"))
|
||||||
|
reference["general"] = _normalize_free_text(reference.get("general"))
|
||||||
|
reference["facts"] = dict(reference.get("facts") or {})
|
||||||
|
return reference
|
||||||
|
if not allow_image_fallback:
|
||||||
|
raise TypeError("Expected a REFERENCE object.")
|
||||||
|
return make_reference(
|
||||||
|
kind="character",
|
||||||
|
image=value,
|
||||||
|
picture_id=picture_id,
|
||||||
|
summary="Plan-bound fallback reference.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _parse_positive_int(value):
|
def _parse_positive_int(value):
|
||||||
text = str(value or "").strip()
|
text = str(value or "").strip()
|
||||||
if not text:
|
if not text:
|
||||||
@@ -1114,35 +1244,26 @@ class DumasH3PlanExtractSceneImagesNode:
|
|||||||
return (passthrough_plan, *images, _connected_image_count(images))
|
return (passthrough_plan, *images, _connected_image_count(images))
|
||||||
|
|
||||||
|
|
||||||
class DumasCharacterHelperNode:
|
class DumasCharacterReferenceNode:
|
||||||
DESCRIPTION = (
|
DESCRIPTION = (
|
||||||
"Build a MiniMax H3-ready character reference prompt and wardrobe sheet "
|
"Build one structured REFERENCE object for a character so H3 can carry "
|
||||||
"from two IMAGE sockets plus simple identity fields, while passing both "
|
"the image, identity description, wardrobe, and facts through one socket."
|
||||||
"images through unchanged."
|
|
||||||
)
|
)
|
||||||
RETURN_TYPES = ("IMAGE", "IMAGE", "STRING", "STRING")
|
RETURN_TYPES = (_REFERENCE_TYPE,)
|
||||||
RETURN_NAMES = ("image1", "image2", "reference_prompt", "wardrobe")
|
RETURN_NAMES = ("reference",)
|
||||||
FUNCTION = "build_character_text"
|
FUNCTION = "build_reference"
|
||||||
CATEGORY = "Dumas/MiniMax"
|
CATEGORY = "Dumas/MiniMax"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"image1": ("IMAGE", {"tooltip": "Primary image to pass through and describe."}),
|
"image": ("IMAGE", {"tooltip": "Character reference image."}),
|
||||||
"image2": ("IMAGE", {"tooltip": "Secondary image to pass through and describe."}),
|
"picture_id": (
|
||||||
"image1_picture_id": (
|
|
||||||
["1", "2", "3", "4", "5", "6", "7", "8", "9"],
|
["1", "2", "3", "4", "5", "6", "7", "8", "9"],
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "1",
|
||||||
"tooltip": "Picture number to mention for image1 in the H3 reference prompt.",
|
"tooltip": "Authoring picture number for this reference.",
|
||||||
},
|
|
||||||
),
|
|
||||||
"image2_picture_id": (
|
|
||||||
["1", "2", "3", "4", "5", "6", "7", "8", "9"],
|
|
||||||
{
|
|
||||||
"default": "2",
|
|
||||||
"tooltip": "Picture number to mention for image2 in the H3 reference prompt.",
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"character_id": (
|
"character_id": (
|
||||||
@@ -1150,7 +1271,7 @@ class DumasCharacterHelperNode:
|
|||||||
{
|
{
|
||||||
"default": "",
|
"default": "",
|
||||||
"multiline": False,
|
"multiline": False,
|
||||||
"tooltip": "Optional character ID string to include in the output text.",
|
"tooltip": "Stable internal identifier for the character.",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"name": (
|
"name": (
|
||||||
@@ -1158,7 +1279,7 @@ class DumasCharacterHelperNode:
|
|||||||
{
|
{
|
||||||
"default": "",
|
"default": "",
|
||||||
"multiline": False,
|
"multiline": False,
|
||||||
"tooltip": "Character name used in the main reference sentences.",
|
"tooltip": "Human-readable character name.",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"alias": (
|
"alias": (
|
||||||
@@ -1166,7 +1287,7 @@ class DumasCharacterHelperNode:
|
|||||||
{
|
{
|
||||||
"default": "",
|
"default": "",
|
||||||
"multiline": False,
|
"multiline": False,
|
||||||
"tooltip": "Optional alternate name, codename, or nickname.",
|
"tooltip": "Comma- or newline-separated aliases for name matching.",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"gender": (
|
"gender": (
|
||||||
@@ -1220,7 +1341,15 @@ class DumasCharacterHelperNode:
|
|||||||
{
|
{
|
||||||
"default": "",
|
"default": "",
|
||||||
"multiline": False,
|
"multiline": False,
|
||||||
"tooltip": "Optional short accent description.",
|
"tooltip": "Optional accent or speaking-style fact.",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"description": (
|
||||||
|
"STRING",
|
||||||
|
{
|
||||||
|
"default": "",
|
||||||
|
"multiline": True,
|
||||||
|
"tooltip": "Persistent physical identity description for the character.",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"general": (
|
"general": (
|
||||||
@@ -1228,7 +1357,7 @@ class DumasCharacterHelperNode:
|
|||||||
{
|
{
|
||||||
"default": "",
|
"default": "",
|
||||||
"multiline": True,
|
"multiline": True,
|
||||||
"tooltip": "Optional non-clothing details appended as the last sentence of the H3 reference prompt.",
|
"tooltip": "Optional freeform notes or extra context.",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"wardrobe": (
|
"wardrobe": (
|
||||||
@@ -1236,18 +1365,16 @@ class DumasCharacterHelperNode:
|
|||||||
{
|
{
|
||||||
"default": "",
|
"default": "",
|
||||||
"multiline": True,
|
"multiline": True,
|
||||||
"tooltip": "Optional H3 wardrobe/channel text. Plain clothing lists are auto-wrapped as 'Name = ...' when a name, alias, or character ID is present.",
|
"tooltip": "Persistent clothing, styling, accessories, or look notes.",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def build_character_text(
|
def build_reference(
|
||||||
self,
|
self,
|
||||||
image1,
|
image,
|
||||||
image2,
|
picture_id,
|
||||||
image1_picture_id,
|
|
||||||
image2_picture_id,
|
|
||||||
character_id,
|
character_id,
|
||||||
name,
|
name,
|
||||||
alias,
|
alias,
|
||||||
@@ -1258,31 +1385,112 @@ class DumasCharacterHelperNode:
|
|||||||
height_feet,
|
height_feet,
|
||||||
height_inches,
|
height_inches,
|
||||||
accent,
|
accent,
|
||||||
|
description,
|
||||||
general,
|
general,
|
||||||
wardrobe,
|
wardrobe,
|
||||||
):
|
):
|
||||||
text = _build_character_helper_text(
|
reference = make_reference(
|
||||||
image1_picture_id,
|
kind="character",
|
||||||
image2_picture_id,
|
image=image,
|
||||||
character_id,
|
explicit_id=character_id,
|
||||||
name,
|
name=name,
|
||||||
alias,
|
aliases=alias,
|
||||||
gender,
|
picture_id=picture_id,
|
||||||
age,
|
description=description,
|
||||||
nationality,
|
wardrobe=wardrobe,
|
||||||
occupation,
|
general=general,
|
||||||
height_feet,
|
facts={
|
||||||
height_inches,
|
"gender": _normalize_free_text(gender),
|
||||||
accent,
|
"age": str(_parse_positive_int(age) or ""),
|
||||||
general,
|
"nationality": _normalize_free_text(nationality),
|
||||||
|
"occupation": _normalize_free_text(occupation),
|
||||||
|
"height_feet": str(height_feet or "").strip(),
|
||||||
|
"height_inches": str(height_inches or "").strip(),
|
||||||
|
"accent": _normalize_free_text(accent),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
wardrobe_text = _build_character_wardrobe_text(
|
return (reference,)
|
||||||
wardrobe,
|
|
||||||
character_id,
|
|
||||||
name,
|
class DumasLocationReferenceNode:
|
||||||
alias,
|
DESCRIPTION = (
|
||||||
|
"Build one structured REFERENCE object for a location or environment so "
|
||||||
|
"H3 can carry the image and environment description through one socket."
|
||||||
|
)
|
||||||
|
RETURN_TYPES = (_REFERENCE_TYPE,)
|
||||||
|
RETURN_NAMES = ("reference",)
|
||||||
|
FUNCTION = "build_reference"
|
||||||
|
CATEGORY = "Dumas/MiniMax"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"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": (
|
||||||
|
"STRING",
|
||||||
|
{
|
||||||
|
"default": "",
|
||||||
|
"multiline": False,
|
||||||
|
"tooltip": "Stable internal identifier for the location.",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"name": (
|
||||||
|
"STRING",
|
||||||
|
{
|
||||||
|
"default": "",
|
||||||
|
"multiline": False,
|
||||||
|
"tooltip": "Human-readable location name.",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"alias": (
|
||||||
|
"STRING",
|
||||||
|
{
|
||||||
|
"default": "",
|
||||||
|
"multiline": False,
|
||||||
|
"tooltip": "Comma- or newline-separated alternate location names.",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"description": (
|
||||||
|
"STRING",
|
||||||
|
{
|
||||||
|
"default": "",
|
||||||
|
"multiline": True,
|
||||||
|
"tooltip": "Persistent environment, layout, and atmosphere description.",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"general": (
|
||||||
|
"STRING",
|
||||||
|
{
|
||||||
|
"default": "",
|
||||||
|
"multiline": True,
|
||||||
|
"tooltip": "Optional freeform location notes.",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_reference(self, image, picture_id, location_id, name, alias, description, general):
|
||||||
|
return (
|
||||||
|
make_reference(
|
||||||
|
kind="location",
|
||||||
|
image=image,
|
||||||
|
explicit_id=location_id,
|
||||||
|
name=name,
|
||||||
|
aliases=alias,
|
||||||
|
picture_id=picture_id,
|
||||||
|
description=description,
|
||||||
|
general=general,
|
||||||
|
facts={},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return (image1, image2, text, wardrobe_text)
|
|
||||||
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
@@ -1291,8 +1499,10 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
"DumasLoadImagesFolder": DumasLoadImagesFolderNode,
|
"DumasLoadImagesFolder": DumasLoadImagesFolderNode,
|
||||||
"DumasH3PlanAttachSceneImages": DumasH3PlanAttachSceneImagesNode,
|
"DumasH3PlanAttachSceneImages": DumasH3PlanAttachSceneImagesNode,
|
||||||
"DumasH3PlanExtractSceneImages": DumasH3PlanExtractSceneImagesNode,
|
"DumasH3PlanExtractSceneImages": DumasH3PlanExtractSceneImagesNode,
|
||||||
"DumasCharacterHelper": DumasCharacterHelperNode,
|
"DumasCharacterReference": DumasCharacterReferenceNode,
|
||||||
"DumasH3CharacterHelper": DumasCharacterHelperNode,
|
"DumasLocationReference": DumasLocationReferenceNode,
|
||||||
|
"DumasCharacterHelper": DumasCharacterReferenceNode,
|
||||||
|
"DumasH3CharacterHelper": DumasCharacterReferenceNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
@@ -1301,6 +1511,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"DumasLoadImagesFolder": "Load Images from Folder Dumas",
|
"DumasLoadImagesFolder": "Load Images from Folder Dumas",
|
||||||
"DumasH3PlanAttachSceneImages": "Dumas H3 Plan Attach Scene Images",
|
"DumasH3PlanAttachSceneImages": "Dumas H3 Plan Attach Scene Images",
|
||||||
"DumasH3PlanExtractSceneImages": "Dumas H3 Plan Extract Scene Images",
|
"DumasH3PlanExtractSceneImages": "Dumas H3 Plan Extract Scene Images",
|
||||||
"DumasCharacterHelper": "Dumas H3 Character Helper",
|
"DumasCharacterReference": "Dumas Character Reference",
|
||||||
"DumasH3CharacterHelper": "Dumas H3 Character Helper",
|
"DumasLocationReference": "Dumas Location Reference",
|
||||||
|
"DumasCharacterHelper": "Dumas Character Reference",
|
||||||
|
"DumasH3CharacterHelper": "Dumas Character Reference",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,9 +154,19 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
self.assertGreaterEqual(fn.cache_info().hits, 2)
|
self.assertGreaterEqual(fn.cache_info().hits, 2)
|
||||||
|
|
||||||
def test_resolve_tagged_refs_preserves_sparse_socket_numbers(self):
|
def test_resolve_tagged_refs_preserves_sparse_socket_numbers(self):
|
||||||
refs = [None, "img2", None, None, None, None, "img7", None, "img9"]
|
refs = [
|
||||||
|
None,
|
||||||
|
{"kind": "character", "image": "img2", "name": "Jon"},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
{"kind": "character", "image": "img7", "name": "Mara"},
|
||||||
|
None,
|
||||||
|
{"kind": "location", "image": "img9", "name": "Watchtower"},
|
||||||
|
]
|
||||||
|
|
||||||
text, images, dropped = self.module.resolve_tagged_refs(
|
text, references, dropped = self.module.resolve_tagged_refs(
|
||||||
"Mara <Picture 7> turns toward Jon <Picture 2> while <Picture 9> watches.",
|
"Mara <Picture 7> turns toward Jon <Picture 2> while <Picture 9> watches.",
|
||||||
refs,
|
refs,
|
||||||
)
|
)
|
||||||
@@ -165,34 +175,53 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
text,
|
text,
|
||||||
"Mara <Picture 2> turns toward Jon <Picture 1> while <Picture 3> watches.",
|
"Mara <Picture 2> turns toward Jon <Picture 1> while <Picture 3> watches.",
|
||||||
)
|
)
|
||||||
self.assertEqual(images, ["img2", "img7", "img9"])
|
self.assertEqual(
|
||||||
|
[self.module._reference_image(ref) for ref in references],
|
||||||
|
["img2", "img7", "img9"],
|
||||||
|
)
|
||||||
self.assertEqual(dropped, [])
|
self.assertEqual(dropped, [])
|
||||||
|
|
||||||
def test_resolve_tagged_refs_drops_unconnected_sparse_slots(self):
|
def test_resolve_tagged_refs_drops_unconnected_sparse_slots(self):
|
||||||
refs = [None, "img2", None, None, None, None, "img7", None, None]
|
refs = [
|
||||||
|
None,
|
||||||
|
{"kind": "character", "image": "img2", "name": "Jon"},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
{"kind": "character", "image": "img7", "name": "Mara"},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
]
|
||||||
|
|
||||||
text, images, dropped = self.module.resolve_tagged_refs(
|
text, references, dropped = self.module.resolve_tagged_refs(
|
||||||
"Use <Picture 7>, skip <Picture 4>, keep <Picture 2>.",
|
"Use <Picture 7>, skip <Picture 4>, keep <Picture 2>.",
|
||||||
refs,
|
refs,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(text, "Use <Picture 2>, skip, keep <Picture 1>.")
|
self.assertEqual(text, "Use <Picture 2>, skip, keep <Picture 1>.")
|
||||||
self.assertEqual(images, ["img2", "img7"])
|
self.assertEqual(
|
||||||
|
[self.module._reference_image(ref) for ref in references],
|
||||||
|
["img2", "img7"],
|
||||||
|
)
|
||||||
self.assertEqual(dropped, [4])
|
self.assertEqual(dropped, [4])
|
||||||
|
|
||||||
def test_shot_references_uses_all_connected_sparse_slots(self):
|
def test_shot_references_uses_all_connected_sparse_slots(self):
|
||||||
refs = [None, "img2", None, "img4", None, None, "img7", None, None]
|
refs = [
|
||||||
|
None,
|
||||||
|
{"kind": "character", "image": "img2"},
|
||||||
|
None,
|
||||||
|
{"kind": "character", "image": "img4"},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
{"kind": "location", "image": "img7"},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
]
|
||||||
|
|
||||||
|
for mode, shot_index in (("auto ref2v", 0), ("first shot", 0), ("every shot", 3)):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.module.shot_references(refs, "auto ref2v", 0, None),
|
[self.module._reference_image(ref) for ref in self.module.shot_references(refs, mode, shot_index, None)],
|
||||||
["img2", "img4", "img7"],
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.module.shot_references(refs, "first shot", 0, None),
|
|
||||||
["img2", "img4", "img7"],
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.module.shot_references(refs, "every shot", 3, None),
|
|
||||||
["img2", "img4", "img7"],
|
["img2", "img4", "img7"],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -200,7 +229,7 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
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.assertIn(f"ref_{index}", optional)
|
||||||
self.assertIn("plan", optional)
|
self.assertIn("plan", optional)
|
||||||
self.assertIn("plan_scene_index", optional)
|
self.assertIn("plan_scene_index", optional)
|
||||||
|
|
||||||
@@ -215,18 +244,64 @@ class DumasH3LongVideosHelperTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(len(refs), 9)
|
self.assertEqual(len(refs), 9)
|
||||||
self.assertIsNone(refs[0])
|
self.assertIsNone(refs[0])
|
||||||
self.assertIs(refs[1], image2)
|
self.assertIs(self.module._reference_image(refs[1]), image2)
|
||||||
self.assertIs(refs[6], image7)
|
self.assertIs(self.module._reference_image(refs[6]), image7)
|
||||||
|
|
||||||
def test_merge_ref_slots_prefers_direct_refs_over_plan_refs(self):
|
def test_merge_ref_slots_prefers_direct_refs_over_plan_refs(self):
|
||||||
merged = self.module._merge_ref_slots(
|
merged = self.module._merge_ref_slots(
|
||||||
(None, "direct2", None, None, "direct5", None, None, None, None),
|
(
|
||||||
("plan1", "plan2", "plan3", None, "plan5", None, "plan7", None, None),
|
None,
|
||||||
|
{"kind": "character", "image": "direct2"},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
{"kind": "character", "image": "direct5"},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"kind": "location", "image": "plan1"},
|
||||||
|
{"kind": "location", "image": "plan2"},
|
||||||
|
{"kind": "location", "image": "plan3"},
|
||||||
|
None,
|
||||||
|
{"kind": "location", "image": "plan5"},
|
||||||
|
None,
|
||||||
|
{"kind": "location", "image": "plan7"},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
merged,
|
[self.module._reference_image(ref) if ref is not None else None for ref in merged],
|
||||||
("plan1", "direct2", "plan3", None, "direct5", None, "plan7", None, None),
|
["plan1", "direct2", "plan3", None, "direct5", None, "plan7", None, None],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_reference_context_matches_character_names_and_location_tags(self):
|
||||||
|
refs = [
|
||||||
|
{"kind": "character", "image": "img1", "name": "Mara", "description": "silver hair", "wardrobe": "red jacket"},
|
||||||
|
{"kind": "location", "image": "img2", "name": "Hangar", "description": "wet concrete floor"},
|
||||||
|
]
|
||||||
|
|
||||||
|
context = self.module._reference_context_for_text(
|
||||||
|
"[Generation 1] Mara crosses the room toward <Picture 2>.",
|
||||||
|
refs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn("Persistent appearance for Mara: silver hair.", context)
|
||||||
|
self.assertIn("Persistent wardrobe/style for Mara: red jacket.", context)
|
||||||
|
self.assertIn("Location context for Hangar: wet concrete floor.", context)
|
||||||
|
|
||||||
|
def test_reference_character_memory_uses_character_wardrobe_only(self):
|
||||||
|
refs = [
|
||||||
|
{"kind": "character", "image": "img1", "name": "Mara", "wardrobe": "red jacket, black boots"},
|
||||||
|
{"kind": "location", "image": "img2", "name": "Hangar", "description": "wet concrete floor", "wardrobe": "should be ignored"},
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.module._reference_character_memory(refs),
|
||||||
|
"Mara = red jacket, black boots",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_ref_mode_defaults_are_ref2v_biased(self):
|
def test_ref_mode_defaults_are_ref2v_biased(self):
|
||||||
|
|||||||
@@ -239,16 +239,13 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
saved_path = FakePILImage.saved_paths[0][0]
|
saved_path = FakePILImage.saved_paths[0][0]
|
||||||
self.assertTrue(os.path.isdir(os.path.dirname(saved_path)))
|
self.assertTrue(os.path.isdir(os.path.dirname(saved_path)))
|
||||||
|
|
||||||
def test_character_helper_passes_through_images_and_formats_text(self):
|
def test_character_reference_builds_structured_reference(self):
|
||||||
node = self.image_nodes.DumasCharacterHelperNode()
|
node = self.image_nodes.DumasCharacterReferenceNode()
|
||||||
image1 = FakeTensorBatch()
|
image = FakeTensorBatch()
|
||||||
image2 = FakeTensorBatch()
|
|
||||||
|
|
||||||
result = node.build_character_text(
|
result = node.build_reference(
|
||||||
image1=image1,
|
image=image,
|
||||||
image2=image2,
|
picture_id="2",
|
||||||
image1_picture_id="2",
|
|
||||||
image2_picture_id="3",
|
|
||||||
character_id="char_dave",
|
character_id="char_dave",
|
||||||
name="Dave",
|
name="Dave",
|
||||||
alias="The Locksmith",
|
alias="The Locksmith",
|
||||||
@@ -259,37 +256,46 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
height_feet="6",
|
height_feet="6",
|
||||||
height_inches="2",
|
height_inches="2",
|
||||||
accent="English",
|
accent="English",
|
||||||
|
description="Square jaw, tired eyes, cropped brown hair.",
|
||||||
general="wears a long grey coat",
|
general="wears a long grey coat",
|
||||||
wardrobe="weathered red flight jacket, grey cargo shorts, black boots",
|
wardrobe="weathered red flight jacket, grey cargo shorts, black boots",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertIs(result[0], image1)
|
reference = result[0]
|
||||||
self.assertIs(result[1], image2)
|
self.assertIs(reference["image"], image)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
result[2],
|
reference,
|
||||||
(
|
{
|
||||||
"<Picture 2> and <Picture 3> reference the same character who is called Dave.\n"
|
"kind": "character",
|
||||||
"<Picture 2> is the primary full-body reference for Dave.\n"
|
"id": "char-dave",
|
||||||
"<Picture 3> is a frontal facial reference for Dave.\n"
|
"name": "Dave",
|
||||||
"Dave is also known as The Locksmith, is male, is 41 years old, is English, works as a detective, is 6 feet 2 inches tall, speaks in an English accent.\n"
|
"aliases": ["The Locksmith"],
|
||||||
"wears a long grey coat."
|
"picture_id": 2,
|
||||||
),
|
"picture_label": "<Picture 2>",
|
||||||
)
|
"image": image,
|
||||||
self.assertEqual(
|
"summary": "Dave shown in <Picture 2>.",
|
||||||
result[3],
|
"description": "Square jaw, tired eyes, cropped brown hair.",
|
||||||
"Dave = 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",
|
||||||
|
"facts": {
|
||||||
|
"gender": "male",
|
||||||
|
"age": "41",
|
||||||
|
"nationality": "English",
|
||||||
|
"occupation": "a detective",
|
||||||
|
"height_feet": "6",
|
||||||
|
"height_inches": "2",
|
||||||
|
"accent": "English",
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_character_helper_handles_missing_optional_fields(self):
|
def test_character_reference_handles_missing_optional_fields(self):
|
||||||
node = self.image_nodes.DumasCharacterHelperNode()
|
node = self.image_nodes.DumasCharacterReferenceNode()
|
||||||
image1 = FakeTensorBatch()
|
image = FakeTensorBatch()
|
||||||
image2 = FakeTensorBatch()
|
|
||||||
|
|
||||||
result = node.build_character_text(
|
result = node.build_reference(
|
||||||
image1=image1,
|
image=image,
|
||||||
image2=image2,
|
picture_id="4",
|
||||||
image1_picture_id="4",
|
|
||||||
image2_picture_id="6",
|
|
||||||
character_id="",
|
character_id="",
|
||||||
name="",
|
name="",
|
||||||
alias="",
|
alias="",
|
||||||
@@ -300,53 +306,50 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
height_feet="",
|
height_feet="",
|
||||||
height_inches="",
|
height_inches="",
|
||||||
accent="",
|
accent="",
|
||||||
|
description="",
|
||||||
general="",
|
general="",
|
||||||
wardrobe="",
|
wardrobe="",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
reference = result[0]
|
||||||
result[2],
|
self.assertEqual(reference["kind"], "character")
|
||||||
(
|
self.assertEqual(reference["picture_id"], 4)
|
||||||
"<Picture 4> and <Picture 6> reference the same character.\n"
|
self.assertEqual(reference["picture_label"], "<Picture 4>")
|
||||||
"<Picture 4> is the primary full-body reference for the character.\n"
|
self.assertEqual(reference["wardrobe"], "")
|
||||||
"<Picture 6> is a frontal facial reference for the character."
|
self.assertEqual(reference["general"], "")
|
||||||
),
|
self.assertEqual(reference["facts"]["age"], "")
|
||||||
)
|
|
||||||
self.assertEqual(result[3], "")
|
|
||||||
|
|
||||||
def test_character_helper_allows_picture_one_and_preserves_full_sheet_wardrobe(self):
|
def test_location_reference_builds_structured_reference(self):
|
||||||
node = self.image_nodes.DumasCharacterHelperNode()
|
node = self.image_nodes.DumasLocationReferenceNode()
|
||||||
image1 = FakeTensorBatch()
|
image = FakeTensorBatch()
|
||||||
image2 = FakeTensorBatch()
|
|
||||||
|
|
||||||
result = node.build_character_text(
|
result = node.build_reference(
|
||||||
image1=image1,
|
image=image,
|
||||||
image2=image2,
|
picture_id="9",
|
||||||
image1_picture_id="1",
|
location_id="coffee-shop-01",
|
||||||
image2_picture_id="9",
|
name="Coffee Shop",
|
||||||
character_id="char_kristy",
|
alias="Cafe Interior",
|
||||||
name="Kristy",
|
description="Warm tungsten lighting, narrow counter, rainy front window.",
|
||||||
alias="",
|
general="Evening ambience, cramped but cozy.",
|
||||||
gender="",
|
|
||||||
age="",
|
|
||||||
nationality="",
|
|
||||||
occupation="",
|
|
||||||
height_feet="",
|
|
||||||
height_inches="",
|
|
||||||
accent="",
|
|
||||||
general="",
|
|
||||||
wardrobe="Kristy = black coat, silver boots",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
result[2],
|
result[0],
|
||||||
(
|
{
|
||||||
"<Picture 1> and <Picture 9> reference the same character who is called Kristy.\n"
|
"kind": "location",
|
||||||
"<Picture 1> is the primary full-body reference for Kristy.\n"
|
"id": "coffee-shop-01",
|
||||||
"<Picture 9> is a frontal facial reference for Kristy."
|
"name": "Coffee Shop",
|
||||||
),
|
"aliases": ["Cafe Interior"],
|
||||||
|
"picture_id": 9,
|
||||||
|
"picture_label": "<Picture 9>",
|
||||||
|
"image": image,
|
||||||
|
"summary": "Coffee Shop shown in <Picture 9>.",
|
||||||
|
"description": "Warm tungsten lighting, narrow counter, rainy front window.",
|
||||||
|
"wardrobe": "",
|
||||||
|
"general": "Evening ambience, cramped but cozy.",
|
||||||
|
"facts": {},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(result[3], "Kristy = black coat, silver boots")
|
|
||||||
|
|
||||||
def test_save_image_returns_ui_entries_for_output_folder(self):
|
def test_save_image_returns_ui_entries_for_output_folder(self):
|
||||||
node = self.image_nodes.DumasSaveImageNode()
|
node = self.image_nodes.DumasSaveImageNode()
|
||||||
|
|||||||
Reference in New Issue
Block a user