Add per-shot H3 beat directives

This commit is contained in:
2026-08-26 22:46:02 +00:00
parent c4cd4121b8
commit f0d51cf6da
5 changed files with 560 additions and 158 deletions
+189 -5
View File
@@ -8,6 +8,17 @@ const DEFAULT_W = 520;
const DEFAULT_H = 340;
const DEFAULT_BEAT = "Describe this beat.";
const STATE_PROPERTY = "dumas_h3_beat_prompt_state";
const CONTINUITY_OPTIONS = ["", "soft carry", "hard cut", "keyframe carry", "handoff ref"];
const REF_MODE_OPTIONS = ["", "auto ref2v", "where tagged", "first shot", "every shot", "every shot + handoff ref"];
const MANAGED_DIRECTIVES = {
seconds: ["seconds", "duration"],
continuity: ["continuity"],
ref_mode: ["ref_mode"],
ref_noise_aug: ["ref_noise_aug"],
anchor_add: ["anchor_add"],
overall_soundscape: ["overall_soundscape", "soundscape"],
non_diegetic_music: ["non_diegetic_music", "music"],
};
const DIRECTIVE_EXAMPLES = [
["wardrobe set", "wardrobe: Maya = grey shorts, red jacket"],
["wardrobe add", "wardrobe: Maya += red jacket"],
@@ -15,6 +26,10 @@ const DIRECTIVE_EXAMPLES = [
["seconds", "seconds: 8"],
["exit", "exit: Maya"],
["enter", "enter: Jon"],
["continuity", "continuity: hard cut"],
["ref_mode", "ref_mode: every shot"],
["ref_noise_aug", "ref_noise_aug: 0.92"],
["anchor_add", "anchor_add: harsh sodium-vapor spill, wet pavement, long-lens compression"],
["overall_soundscape", "overall_soundscape: soft rain, distant traffic"],
["non_diegetic_music", "non_diegetic_music: tense analog synth pulse"],
["soundscape", "soundscape: fluorescent room tone, faint HVAC hum"],
@@ -113,6 +128,40 @@ function injectCSS() {
flex-wrap: wrap;
gap: 6px;
}
.dh3bp-controls {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
margin-bottom: 10px;
}
.dh3bp-control {
display: flex;
flex-direction: column;
gap: 4px;
}
.dh3bp-control-wide {
grid-column: 1 / -1;
}
.dh3bp-control-label {
color: #b8bec8;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.dh3bp-input,
.dh3bp-select {
width: 100%;
box-sizing: border-box;
background: #121418;
color: #e8e8e8;
border: 1px solid #3c414a;
border-radius: 8px;
padding: 7px 9px;
font: 12px/1.35 "Segoe UI", sans-serif;
}
.dh3bp-input::placeholder {
color: #7f8793;
}
.dh3bp-directive {
background: #252a33;
color: #d4d9e1;
@@ -202,6 +251,41 @@ function appendDirectiveText(currentText, example) {
return `${trimmed}\n${example}`;
}
function splitBeatLines(text) {
return String(text || "").split(/\n/);
}
function readDirectiveValue(text, directiveNames) {
let value = "";
for (const line of splitBeatLines(text)) {
const trimmed = line.trim();
for (const name of directiveNames) {
const lower = name.toLowerCase();
if (trimmed.toLowerCase().startsWith(`${lower}:`)) {
value = trimmed.slice(trimmed.indexOf(":") + 1).trim();
}
}
}
return value;
}
function stripDirectiveValues(text, directiveNames) {
const lowered = directiveNames.map((name) => name.toLowerCase());
const kept = splitBeatLines(text).filter((line) => {
const trimmed = line.trim().toLowerCase();
return !lowered.some((name) => trimmed.startsWith(`${name}:`));
});
return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
}
function setDirectiveValue(text, canonicalName, directiveNames, value) {
const cleaned = stripDirectiveValues(text, directiveNames);
const trimmedValue = String(value || "").trim();
if (!trimmedValue) return cleaned;
const directiveLine = `${canonicalName}: ${trimmedValue}`;
return cleaned ? `${directiveLine}\n${cleaned}` : directiveLine;
}
function renderUI(node) {
const ui = node._dh3bpUI;
if (!ui) return;
@@ -246,6 +330,108 @@ function renderUI(node) {
});
textarea.addEventListener("keydown", (event) => event.stopImmediatePropagation());
const applyTextUpdate = (nextText) => {
const next = readState(node);
next.beats[index].text = nextText;
writeState(node, next);
textarea.value = nextText;
updateTextareaHeight(textarea);
};
const controls = document.createElement("div");
controls.className = "dh3bp-controls";
const buildField = ({ labelText, className = "", input }) => {
const wrap = document.createElement("label");
wrap.className = `dh3bp-control ${className}`.trim();
const labelEl = document.createElement("div");
labelEl.className = "dh3bp-control-label";
labelEl.textContent = labelText;
wrap.append(labelEl, input);
return wrap;
};
const secondsInput = document.createElement("input");
secondsInput.className = "dh3bp-input";
secondsInput.type = "text";
secondsInput.placeholder = "8";
secondsInput.value = readDirectiveValue(beat.text, MANAGED_DIRECTIVES.seconds);
secondsInput.addEventListener("input", () => {
applyTextUpdate(setDirectiveValue(textarea.value, "seconds", MANAGED_DIRECTIVES.seconds, secondsInput.value));
});
const continuitySelect = document.createElement("select");
continuitySelect.className = "dh3bp-select";
CONTINUITY_OPTIONS.forEach((value) => {
const option = document.createElement("option");
option.value = value;
option.textContent = value || "Default";
continuitySelect.appendChild(option);
});
continuitySelect.value = readDirectiveValue(beat.text, MANAGED_DIRECTIVES.continuity);
continuitySelect.addEventListener("change", () => {
applyTextUpdate(setDirectiveValue(textarea.value, "continuity", MANAGED_DIRECTIVES.continuity, continuitySelect.value));
});
const refModeSelect = document.createElement("select");
refModeSelect.className = "dh3bp-select";
REF_MODE_OPTIONS.forEach((value) => {
const option = document.createElement("option");
option.value = value;
option.textContent = value || "Global";
refModeSelect.appendChild(option);
});
refModeSelect.value = readDirectiveValue(beat.text, MANAGED_DIRECTIVES.ref_mode);
refModeSelect.addEventListener("change", () => {
applyTextUpdate(setDirectiveValue(textarea.value, "ref_mode", MANAGED_DIRECTIVES.ref_mode, refModeSelect.value));
});
const refNoiseInput = document.createElement("input");
refNoiseInput.className = "dh3bp-input";
refNoiseInput.type = "text";
refNoiseInput.placeholder = "0.95";
refNoiseInput.value = readDirectiveValue(beat.text, MANAGED_DIRECTIVES.ref_noise_aug);
refNoiseInput.addEventListener("input", () => {
applyTextUpdate(setDirectiveValue(textarea.value, "ref_noise_aug", MANAGED_DIRECTIVES.ref_noise_aug, refNoiseInput.value));
});
const anchorInput = document.createElement("input");
anchorInput.className = "dh3bp-input";
anchorInput.type = "text";
anchorInput.placeholder = "extra per-shot style treatment";
anchorInput.value = readDirectiveValue(beat.text, MANAGED_DIRECTIVES.anchor_add);
anchorInput.addEventListener("input", () => {
applyTextUpdate(setDirectiveValue(textarea.value, "anchor_add", MANAGED_DIRECTIVES.anchor_add, anchorInput.value));
});
const soundscapeInput = document.createElement("input");
soundscapeInput.className = "dh3bp-input";
soundscapeInput.type = "text";
soundscapeInput.placeholder = "faint traffic, loose sign rattle";
soundscapeInput.value = readDirectiveValue(beat.text, MANAGED_DIRECTIVES.overall_soundscape);
soundscapeInput.addEventListener("input", () => {
applyTextUpdate(setDirectiveValue(textarea.value, "overall_soundscape", MANAGED_DIRECTIVES.overall_soundscape, soundscapeInput.value));
});
const musicInput = document.createElement("input");
musicInput.className = "dh3bp-input";
musicInput.type = "text";
musicInput.placeholder = "low pulsing synth tension";
musicInput.value = readDirectiveValue(beat.text, MANAGED_DIRECTIVES.non_diegetic_music);
musicInput.addEventListener("input", () => {
applyTextUpdate(setDirectiveValue(textarea.value, "non_diegetic_music", MANAGED_DIRECTIVES.non_diegetic_music, musicInput.value));
});
controls.append(
buildField({ labelText: "Seconds", input: secondsInput }),
buildField({ labelText: "Continuity", input: continuitySelect }),
buildField({ labelText: "Ref Mode", input: refModeSelect }),
buildField({ labelText: "Ref Noise Aug", input: refNoiseInput }),
buildField({ labelText: "Anchor Add", className: "dh3bp-control-wide", input: anchorInput }),
buildField({ labelText: "Shot Soundscape", className: "dh3bp-control-wide", input: soundscapeInput }),
buildField({ labelText: "Shot Music", className: "dh3bp-control-wide", input: musicInput }),
);
const directives = document.createElement("div");
directives.className = "dh3bp-directives";
@@ -256,15 +442,13 @@ function renderUI(node) {
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);
applyTextUpdate(appendDirectiveText(textarea.value, example));
renderUI(node);
});
directives.appendChild(button);
});
card.append(head, textarea, directives);
card.append(head, textarea, controls, directives);
ui.list.appendChild(card);
updateTextareaHeight(textarea);
});
@@ -288,7 +472,7 @@ function setupNode(node) {
title.textContent = "Beat Prompt Builder";
const subtitle = document.createElement("div");
subtitle.className = "dh3bp-subtitle";
subtitle.textContent = "One textbox per H3 beat. Use the buttons for seconds and quick wardrobe set/add/remove syntax.";
subtitle.textContent = "One textbox per H3 beat, plus per-shot controls for timing, ref behavior, continuity, anchor adds, and audio directives.";
titleWrap.append(title, subtitle);
const addButton = document.createElement("button");