Add per-shot H3 beat directives
This commit is contained in:
+189
-5
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user