Add H3 beat prompt builder node
This commit is contained in:
@@ -22,6 +22,10 @@ from .dumas_h3_inspector import (
|
|||||||
NODE_CLASS_MAPPINGS as H3_INSPECTOR_NODE_CLASS_MAPPINGS,
|
NODE_CLASS_MAPPINGS as H3_INSPECTOR_NODE_CLASS_MAPPINGS,
|
||||||
NODE_DISPLAY_NAME_MAPPINGS as H3_INSPECTOR_NODE_DISPLAY_NAME_MAPPINGS,
|
NODE_DISPLAY_NAME_MAPPINGS as H3_INSPECTOR_NODE_DISPLAY_NAME_MAPPINGS,
|
||||||
)
|
)
|
||||||
|
from .dumas_h3_beat_prompt import (
|
||||||
|
NODE_CLASS_MAPPINGS as H3_BEAT_PROMPT_NODE_CLASS_MAPPINGS,
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS as H3_BEAT_PROMPT_NODE_DISPLAY_NAME_MAPPINGS,
|
||||||
|
)
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {}
|
NODE_CLASS_MAPPINGS = {}
|
||||||
NODE_CLASS_MAPPINGS.update(JSON_NODE_CLASS_MAPPINGS)
|
NODE_CLASS_MAPPINGS.update(JSON_NODE_CLASS_MAPPINGS)
|
||||||
@@ -29,6 +33,7 @@ NODE_CLASS_MAPPINGS.update(IMAGE_NODE_CLASS_MAPPINGS)
|
|||||||
NODE_CLASS_MAPPINGS.update(H3_LONGVIDEO_NODE_CLASS_MAPPINGS)
|
NODE_CLASS_MAPPINGS.update(H3_LONGVIDEO_NODE_CLASS_MAPPINGS)
|
||||||
NODE_CLASS_MAPPINGS.update(H3_SHOT_LENGTH_NODE_CLASS_MAPPINGS)
|
NODE_CLASS_MAPPINGS.update(H3_SHOT_LENGTH_NODE_CLASS_MAPPINGS)
|
||||||
NODE_CLASS_MAPPINGS.update(H3_INSPECTOR_NODE_CLASS_MAPPINGS)
|
NODE_CLASS_MAPPINGS.update(H3_INSPECTOR_NODE_CLASS_MAPPINGS)
|
||||||
|
NODE_CLASS_MAPPINGS.update(H3_BEAT_PROMPT_NODE_CLASS_MAPPINGS)
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||||
NODE_DISPLAY_NAME_MAPPINGS.update(JSON_NODE_DISPLAY_NAME_MAPPINGS)
|
NODE_DISPLAY_NAME_MAPPINGS.update(JSON_NODE_DISPLAY_NAME_MAPPINGS)
|
||||||
@@ -36,6 +41,7 @@ NODE_DISPLAY_NAME_MAPPINGS.update(IMAGE_NODE_DISPLAY_NAME_MAPPINGS)
|
|||||||
NODE_DISPLAY_NAME_MAPPINGS.update(H3_LONGVIDEO_NODE_DISPLAY_NAME_MAPPINGS)
|
NODE_DISPLAY_NAME_MAPPINGS.update(H3_LONGVIDEO_NODE_DISPLAY_NAME_MAPPINGS)
|
||||||
NODE_DISPLAY_NAME_MAPPINGS.update(H3_SHOT_LENGTH_NODE_DISPLAY_NAME_MAPPINGS)
|
NODE_DISPLAY_NAME_MAPPINGS.update(H3_SHOT_LENGTH_NODE_DISPLAY_NAME_MAPPINGS)
|
||||||
NODE_DISPLAY_NAME_MAPPINGS.update(H3_INSPECTOR_NODE_DISPLAY_NAME_MAPPINGS)
|
NODE_DISPLAY_NAME_MAPPINGS.update(H3_INSPECTOR_NODE_DISPLAY_NAME_MAPPINGS)
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS.update(H3_BEAT_PROMPT_NODE_DISPLAY_NAME_MAPPINGS)
|
||||||
|
|
||||||
WEB_DIRECTORY = "./js"
|
WEB_DIRECTORY = "./js"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
_DEFAULT_BEAT = "Describe this beat."
|
||||||
|
_DEFAULT_STATE = {"beats": [{"text": _DEFAULT_BEAT}]}
|
||||||
|
|
||||||
|
|
||||||
|
def _clone_default_state():
|
||||||
|
return {"beats": [{"text": _DEFAULT_BEAT}]}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_beat_prompt_state(value):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
raw = value
|
||||||
|
else:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if not text:
|
||||||
|
return _clone_default_state()
|
||||||
|
try:
|
||||||
|
raw = json.loads(text)
|
||||||
|
except Exception:
|
||||||
|
return _clone_default_state()
|
||||||
|
|
||||||
|
beats = []
|
||||||
|
for item in list(raw.get("beats") or []):
|
||||||
|
if isinstance(item, dict):
|
||||||
|
text = str(item.get("text") or "")
|
||||||
|
else:
|
||||||
|
text = str(item or "")
|
||||||
|
beats.append({"text": text})
|
||||||
|
|
||||||
|
if not beats:
|
||||||
|
return _clone_default_state()
|
||||||
|
return {"beats": beats}
|
||||||
|
|
||||||
|
|
||||||
|
def _assemble_beat_prompt(state):
|
||||||
|
parsed = _parse_beat_prompt_state(state)
|
||||||
|
chunks = []
|
||||||
|
for beat in parsed["beats"]:
|
||||||
|
text = str(beat.get("text") or "").strip()
|
||||||
|
if text:
|
||||||
|
chunks.append(text)
|
||||||
|
return "\n\n".join(chunks)
|
||||||
|
|
||||||
|
|
||||||
|
class DumasH3BeatPromptNode:
|
||||||
|
DESCRIPTION = (
|
||||||
|
"Build a MiniMax H3 prompt from one textbox per beat, with a front-end beat "
|
||||||
|
"editor that can append directive examples such as wardrobe, seconds, exit, and music."
|
||||||
|
)
|
||||||
|
RETURN_TYPES = ("STRING",)
|
||||||
|
RETURN_NAMES = ("prompt",)
|
||||||
|
FUNCTION = "build_prompt"
|
||||||
|
CATEGORY = "Dumas/MiniMax"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {},
|
||||||
|
"hidden": {
|
||||||
|
"H3BeatPromptState": ("STRING", {"default": json.dumps(_DEFAULT_STATE)}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_prompt(self, H3BeatPromptState=""):
|
||||||
|
return (_assemble_beat_prompt(H3BeatPromptState),)
|
||||||
|
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"DumasH3BeatPrompt": DumasH3BeatPromptNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"DumasH3BeatPrompt": "Dumas H3 Beat Prompt",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"DumasH3BeatPromptNode",
|
||||||
|
"NODE_CLASS_MAPPINGS",
|
||||||
|
"NODE_DISPLAY_NAME_MAPPINGS",
|
||||||
|
"_assemble_beat_prompt",
|
||||||
|
"_parse_beat_prompt_state",
|
||||||
|
]
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
import { app } from "/scripts/app.js";
|
||||||
|
import { applyAdaptiveCanvasOnly } from "../shared/nodes2.mjs";
|
||||||
|
|
||||||
|
const COMFY_CLASS = "DumasH3BeatPrompt";
|
||||||
|
const HIDDEN_INPUT_NAME = "H3BeatPromptState";
|
||||||
|
const MIN_W = 420;
|
||||||
|
const DEFAULT_W = 520;
|
||||||
|
const DEFAULT_H = 340;
|
||||||
|
const DEFAULT_BEAT = "Describe this beat.";
|
||||||
|
const DIRECTIVE_EXAMPLES = [
|
||||||
|
["wardrobe", "wardrobe: Maya = grey shorts, red jacket"],
|
||||||
|
["seconds", "seconds: 8"],
|
||||||
|
["duration", "duration: 8 seconds"],
|
||||||
|
["exit", "exit: Maya"],
|
||||||
|
["enter", "enter: Jon"],
|
||||||
|
["overall_soundscape", "overall_soundscape: soft rain, distant traffic"],
|
||||||
|
["non_diegetic_music", "non_diegetic_music: tense analog synth pulse"],
|
||||||
|
["soundscape", "soundscape: fluorescent room tone, faint HVAC hum"],
|
||||||
|
["music", "music: low ominous cello and sparse percussion"],
|
||||||
|
];
|
||||||
|
|
||||||
|
function injectCSS() {
|
||||||
|
if (document.getElementById("dumas-h3-beat-prompt-css")) return;
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.id = "dumas-h3-beat-prompt-css";
|
||||||
|
style.textContent = `
|
||||||
|
.dh3bp-root {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
color: #e8e8e8;
|
||||||
|
font: 12px/1.35 "Segoe UI", sans-serif;
|
||||||
|
padding: 8px 10px 10px;
|
||||||
|
}
|
||||||
|
.dh3bp-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.dh3bp-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
.dh3bp-subtitle {
|
||||||
|
color: #9da3ae;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.dh3bp-add {
|
||||||
|
background: #e66a2c;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #f28b57;
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.dh3bp-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.dh3bp-beat {
|
||||||
|
background: #1e2025;
|
||||||
|
border: 1px solid #363a42;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255,255,255,0.02);
|
||||||
|
}
|
||||||
|
.dh3bp-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.dh3bp-label {
|
||||||
|
color: #f3f4f6;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.dh3bp-remove {
|
||||||
|
background: transparent;
|
||||||
|
color: #f59a9a;
|
||||||
|
border: 1px solid #5a2c2c;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.dh3bp-remove:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.dh3bp-text {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 96px;
|
||||||
|
resize: vertical;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #121418;
|
||||||
|
color: #e8e8e8;
|
||||||
|
border: 1px solid #3c414a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font: 12px/1.4 "Consolas", "SFMono-Regular", monospace;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.dh3bp-directives {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.dh3bp-directive {
|
||||||
|
background: #252a33;
|
||||||
|
color: #d4d9e1;
|
||||||
|
border: 1px solid #414754;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.dh3bp-directive:hover,
|
||||||
|
.dh3bp-remove:hover,
|
||||||
|
.dh3bp-add:hover {
|
||||||
|
filter: brightness(1.08);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultState() {
|
||||||
|
return { beats: [{ text: DEFAULT_BEAT }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeState(value) {
|
||||||
|
let parsed = value;
|
||||||
|
if (typeof parsed === "string") {
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(parsed);
|
||||||
|
} catch (_error) {
|
||||||
|
parsed = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!parsed || typeof parsed !== "object") return defaultState();
|
||||||
|
const beats = Array.isArray(parsed.beats) ? parsed.beats : [];
|
||||||
|
const normalized = beats.map((beat) => ({
|
||||||
|
text: typeof beat?.text === "string" ? beat.text : String(beat?.text || ""),
|
||||||
|
}));
|
||||||
|
return normalized.length ? { beats: normalized } : defaultState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function readState(node) {
|
||||||
|
return normalizeState(node._dh3bpState || findWidget(node, HIDDEN_INPUT_NAME)?.value || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function findWidget(node, name) {
|
||||||
|
return (node.widgets || []).find((widget) => widget?.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideWidget(node, name) {
|
||||||
|
const widget = findWidget(node, name);
|
||||||
|
if (!widget) return;
|
||||||
|
widget.type = "hidden";
|
||||||
|
widget.computeSize = () => [0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeState(node, state) {
|
||||||
|
const normalized = normalizeState(state);
|
||||||
|
const serialized = JSON.stringify(normalized);
|
||||||
|
node._dh3bpState = serialized;
|
||||||
|
const widget = findWidget(node, HIDDEN_INPUT_NAME);
|
||||||
|
if (widget) widget.value = serialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTextareaHeight(textarea) {
|
||||||
|
textarea.style.height = "auto";
|
||||||
|
textarea.style.height = `${Math.max(96, textarea.scrollHeight)}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendDirectiveText(currentText, example) {
|
||||||
|
const trimmed = String(currentText || "").replace(/\s+$/, "");
|
||||||
|
if (!trimmed) return example;
|
||||||
|
if (trimmed.includes(example)) return trimmed;
|
||||||
|
return `${trimmed}\n${example}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUI(node) {
|
||||||
|
const ui = node._dh3bpUI;
|
||||||
|
if (!ui) return;
|
||||||
|
|
||||||
|
const state = readState(node);
|
||||||
|
ui.list.innerHTML = "";
|
||||||
|
|
||||||
|
state.beats.forEach((beat, index) => {
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "dh3bp-beat";
|
||||||
|
|
||||||
|
const head = document.createElement("div");
|
||||||
|
head.className = "dh3bp-head";
|
||||||
|
|
||||||
|
const label = document.createElement("div");
|
||||||
|
label.className = "dh3bp-label";
|
||||||
|
label.textContent = `Beat ${index + 1}`;
|
||||||
|
|
||||||
|
const remove = document.createElement("button");
|
||||||
|
remove.className = "dh3bp-remove";
|
||||||
|
remove.type = "button";
|
||||||
|
remove.textContent = "Remove";
|
||||||
|
remove.disabled = state.beats.length <= 1;
|
||||||
|
remove.addEventListener("click", () => {
|
||||||
|
const next = readState(node);
|
||||||
|
next.beats.splice(index, 1);
|
||||||
|
writeState(node, next);
|
||||||
|
renderUI(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
head.append(label, remove);
|
||||||
|
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.className = "dh3bp-text";
|
||||||
|
textarea.placeholder = "Describe this beat...";
|
||||||
|
textarea.value = beat.text || "";
|
||||||
|
textarea.addEventListener("input", () => {
|
||||||
|
const next = readState(node);
|
||||||
|
next.beats[index].text = textarea.value;
|
||||||
|
writeState(node, next);
|
||||||
|
updateTextareaHeight(textarea);
|
||||||
|
});
|
||||||
|
textarea.addEventListener("keydown", (event) => event.stopImmediatePropagation());
|
||||||
|
|
||||||
|
const directives = document.createElement("div");
|
||||||
|
directives.className = "dh3bp-directives";
|
||||||
|
|
||||||
|
DIRECTIVE_EXAMPLES.forEach(([labelText, example]) => {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.className = "dh3bp-directive";
|
||||||
|
button.type = "button";
|
||||||
|
button.textContent = labelText;
|
||||||
|
button.title = example;
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const next = readState(node);
|
||||||
|
next.beats[index].text = appendDirectiveText(next.beats[index].text, example);
|
||||||
|
writeState(node, next);
|
||||||
|
renderUI(node);
|
||||||
|
});
|
||||||
|
directives.appendChild(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
card.append(head, textarea, directives);
|
||||||
|
ui.list.appendChild(card);
|
||||||
|
updateTextareaHeight(textarea);
|
||||||
|
});
|
||||||
|
|
||||||
|
node.setDirtyCanvas?.(true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupNode(node) {
|
||||||
|
injectCSS();
|
||||||
|
hideWidget(node, HIDDEN_INPUT_NAME);
|
||||||
|
|
||||||
|
const root = document.createElement("div");
|
||||||
|
root.className = "dh3bp-root";
|
||||||
|
|
||||||
|
const toolbar = document.createElement("div");
|
||||||
|
toolbar.className = "dh3bp-toolbar";
|
||||||
|
|
||||||
|
const titleWrap = document.createElement("div");
|
||||||
|
const title = document.createElement("div");
|
||||||
|
title.className = "dh3bp-title";
|
||||||
|
title.textContent = "Beat Prompt Builder";
|
||||||
|
const subtitle = document.createElement("div");
|
||||||
|
subtitle.className = "dh3bp-subtitle";
|
||||||
|
subtitle.textContent = "One textbox per H3 beat. Buttons append directive examples into that beat.";
|
||||||
|
titleWrap.append(title, subtitle);
|
||||||
|
|
||||||
|
const addButton = document.createElement("button");
|
||||||
|
addButton.className = "dh3bp-add";
|
||||||
|
addButton.type = "button";
|
||||||
|
addButton.textContent = "+ Add Beat";
|
||||||
|
addButton.addEventListener("click", () => {
|
||||||
|
const next = readState(node);
|
||||||
|
next.beats.push({ text: "" });
|
||||||
|
writeState(node, next);
|
||||||
|
renderUI(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
toolbar.append(titleWrap, addButton);
|
||||||
|
|
||||||
|
const list = document.createElement("div");
|
||||||
|
list.className = "dh3bp-list";
|
||||||
|
|
||||||
|
root.append(toolbar, list);
|
||||||
|
|
||||||
|
node._dh3bpUI = { root, list };
|
||||||
|
const widget = node.addDOMWidget("dumas_h3_beat_prompt", "custom", root, {
|
||||||
|
getValue: () => null,
|
||||||
|
setValue: () => {},
|
||||||
|
serialize: false,
|
||||||
|
getMinHeight: () => 180,
|
||||||
|
});
|
||||||
|
widget.computeLayoutSize = () => ({ minHeight: 180, minWidth: 1 });
|
||||||
|
applyAdaptiveCanvasOnly(widget);
|
||||||
|
|
||||||
|
node.size[0] = Math.max(node.size[0] || 0, DEFAULT_W);
|
||||||
|
node.size[1] = Math.max(node.size[1] || 0, DEFAULT_H);
|
||||||
|
|
||||||
|
writeState(node, readState(node));
|
||||||
|
renderUI(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectNodes(graph, out) {
|
||||||
|
if (!graph) return;
|
||||||
|
const nodes = graph._nodes || graph.nodes || [];
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node?.comfyClass === COMFY_CLASS) out.push(node);
|
||||||
|
const inner = node?.subgraph || node?.graph || node?._graph;
|
||||||
|
if (inner && inner !== graph) collectNodes(inner, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchNode(nodes, promptId) {
|
||||||
|
let node = nodes.find((entry) => String(entry.id) === String(promptId));
|
||||||
|
if (node) return node;
|
||||||
|
const tail = String(promptId).split(":").pop();
|
||||||
|
return nodes.find((entry) => String(entry.id) === tail) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectState(result) {
|
||||||
|
const output = result?.output;
|
||||||
|
if (!output) return;
|
||||||
|
const nodes = [];
|
||||||
|
collectNodes(app.graph, nodes);
|
||||||
|
for (const id in output) {
|
||||||
|
const entry = output[id];
|
||||||
|
if (!entry || entry.class_type !== COMFY_CLASS) continue;
|
||||||
|
const node = matchNode(nodes, id);
|
||||||
|
if (!node) continue;
|
||||||
|
if (!entry.inputs) entry.inputs = {};
|
||||||
|
entry.inputs[HIDDEN_INPUT_NAME] = node._dh3bpState || JSON.stringify(defaultState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function installGraphToPromptHook() {
|
||||||
|
if (app._dh3bpGraphPatched) return;
|
||||||
|
app._dh3bpGraphPatched = true;
|
||||||
|
const original = app.graphToPrompt.bind(app);
|
||||||
|
app.graphToPrompt = async function graphToPromptPatched(...args) {
|
||||||
|
const result = await original(...args);
|
||||||
|
injectState(result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
app.registerExtension({
|
||||||
|
name: "Dumas.H3BeatPrompt",
|
||||||
|
setup() {
|
||||||
|
installGraphToPromptHook();
|
||||||
|
},
|
||||||
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||||
|
if (nodeData?.name !== COMFY_CLASS) return;
|
||||||
|
|
||||||
|
const originalNodeCreated = nodeType.prototype.onNodeCreated;
|
||||||
|
nodeType.prototype.onNodeCreated = function onNodeCreated() {
|
||||||
|
const result = originalNodeCreated?.apply(this, arguments);
|
||||||
|
setupNode(this);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalConfigure = nodeType.prototype.onConfigure;
|
||||||
|
nodeType.prototype.onConfigure = function onConfigure() {
|
||||||
|
const result = originalConfigure?.apply(this, arguments);
|
||||||
|
this._dh3bpState = findWidget(this, HIDDEN_INPUT_NAME)?.value || this._dh3bpState;
|
||||||
|
queueMicrotask(() => renderUI(this));
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalDrawForeground = nodeType.prototype.onDrawForeground;
|
||||||
|
nodeType.prototype.onDrawForeground = function onDrawForeground(ctx) {
|
||||||
|
const result = originalDrawForeground?.call(this, ctx);
|
||||||
|
this.size[0] = Math.max(this.size[0], MIN_W);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import importlib
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class DumasH3BeatPromptTests(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.module = importlib.import_module("dumas_h3_beat_prompt")
|
||||||
|
|
||||||
|
def test_parse_state_falls_back_to_default(self):
|
||||||
|
state = self.module._parse_beat_prompt_state("not json")
|
||||||
|
self.assertEqual(
|
||||||
|
state,
|
||||||
|
{"beats": [{"text": "Describe this beat."}]},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_assemble_prompt_joins_beats_with_blank_lines(self):
|
||||||
|
prompt = self.module._assemble_beat_prompt(
|
||||||
|
{
|
||||||
|
"beats": [
|
||||||
|
{"text": "A woman enters the room."},
|
||||||
|
{"text": "wardrobe: Maya = red jacket\nShe sits at the table."},
|
||||||
|
{"text": " "},
|
||||||
|
{"text": "music: low synth pulse"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
prompt,
|
||||||
|
(
|
||||||
|
"A woman enters the room.\n\n"
|
||||||
|
"wardrobe: Maya = red jacket\nShe sits at the table.\n\n"
|
||||||
|
"music: low synth pulse"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_node_build_prompt_uses_hidden_state(self):
|
||||||
|
node = self.module.DumasH3BeatPromptNode()
|
||||||
|
result = node.build_prompt(
|
||||||
|
'{"beats":[{"text":"Beat one"},{"text":"Beat two"}]}'
|
||||||
|
)
|
||||||
|
self.assertEqual(result, ("Beat one\n\nBeat two",))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user