fix: a pin repaints its tile at once; a rebuilt focused tile keeps focus; clock-time guard needs a real cut phrase; plain nouns (back, pan) can be infused; infusion hint names its prerequisites
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
4bb2ac56cc
commit
57ea1a70b6
@@ -81,7 +81,7 @@ function collection({ items = [], empty = EMPTY, selection = [], onActivate, onC
|
||||
kept = next;
|
||||
const now = [...host.children];
|
||||
// a rebuilt tile swaps in place when every other tile stays where it is, so a tile with the keyboard focus keeps it
|
||||
if (now.length === cells.length && cells.every((cell, i) => cell === now[i] || !cell.parentNode)) cells.forEach((cell, i) => { if (cell !== now[i]) now[i].replaceWith(cell); });
|
||||
if (now.length === cells.length && cells.every((cell, i) => cell === now[i] || !cell.parentNode)) cells.forEach((cell, i) => { if (cell === now[i]) return; const had = now[i] === document.activeElement; now[i].replaceWith(cell); if (had) cell.focus({ preventScroll: true }); });
|
||||
else if (now.length !== cells.length || cells.some((cell, i) => cell !== now[i])) host.replaceChildren(...cells);
|
||||
}
|
||||
draw();
|
||||
|
||||
@@ -18,7 +18,6 @@ const mediaField = (app, p, sc, { label, hint, key, multiple, kinds, empty }) =>
|
||||
c.button.sm({ label: "✕", tone: "ghost", disabled: !ids.length, title: "Clear", onClick: () => set([]) })] }) });
|
||||
};
|
||||
|
||||
/** One "Pin reference to the slot" per reference: a pinned one keeps its R-number when an earlier reference is switched off. */
|
||||
/** A number-or-mode pair: "Project default / Timeline trim / Custom", and the number only when Custom. */
|
||||
const lengthField = (p, sc, label, modeKey, valueKey, shown, grid) => {
|
||||
const mode = sc[modeKey] || "project";
|
||||
|
||||
@@ -122,3 +122,23 @@ test("redrawing the bin keeps keyboard focus on the tile that has it", async ()
|
||||
const now = [...host.querySelectorAll(".cx-cell")].find((x) => x.textContent.includes("dog.png")); assert.ok(was, "focusable"); assert.equal(now, tile, "same tile node"); assert.equal(document.activeElement, tile, "the focused tile survives the redraw");
|
||||
host.remove();
|
||||
});
|
||||
|
||||
test("pinning the first reference repaints its tile even though no number moves; toggling the focused tile keeps its focus", async () => {
|
||||
const fixed = {};
|
||||
const project = { selected: { id: "s1", references: ["m1", "m2"] }, project: { fixed_references: fixed }, setScene: () => {} };
|
||||
const api = { media: async () => ({ media: [{ id: "m1", name: "cat.png", kind: "image" }, { id: "m2", name: "dog.png", kind: "image" }] }) };
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
let fire;
|
||||
hub.setup({ host, app: { project, api, mediaPeek: {}, say: () => {}, on: (cb) => { fire = cb; return () => {}; } } });
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
const cell = (n) => [...host.querySelectorAll(".cx-cell")].find((x) => x.textContent.includes(n));
|
||||
fixed.m1 = 1;
|
||||
fire("project");
|
||||
assert.match(cell("cat.png").textContent, /📌R1/, "pinned: the pin shows");
|
||||
cell("dog.png").focus();
|
||||
project.selected.references = ["m1"];
|
||||
fire("project");
|
||||
assert.equal(document.activeElement, cell("dog.png"), "the focused tile, rebuilt, keeps the focus");
|
||||
host.remove();
|
||||
});
|
||||
|
||||
@@ -114,7 +114,7 @@ export default {
|
||||
let drawn = "";
|
||||
function draw(force) {
|
||||
const sc = p.selected, plan = sc ? sceneReferences(sc, (p.project || {}).fixed_references) : { ids: [], pins: {} }, refs = plan.ids, nums = referenceNumbers(refs, plan.pins);
|
||||
const key = `${(sc || {}).source_image}|${refs}|${nums}|${items.map((m) => m.id)}|${selecting}|${[...chosen]}|${show.filter}`;
|
||||
const key = `${(sc || {}).source_image}|${refs}|${nums}|${JSON.stringify(plan.pins)}|${items.map((m) => m.id)}|${selecting}|${[...chosen]}|${show.filter}`;
|
||||
if (!force && key === drawn) return; // typing elsewhere must not rebuild the thumbnails
|
||||
drawn = key;
|
||||
const visible = new Set(items.filter((m) => show.filter === "all" || m.kind === show.filter).map((m) => m.id));
|
||||
|
||||
@@ -52,7 +52,7 @@ SETTINGS = {
|
||||
"uneven_shots_chance": {"type": "float", "default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05, "label": "Chance of an uneven shot length",
|
||||
"hint": "0 = equal shots. 1 = every shot's length is changed, busier text tending to run longer. Needs a cut option on. Ratings nudge the values in between."},
|
||||
"focus_infusion_chance": {"type": "float", "default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05, "label": "Chance of focus infusion",
|
||||
"hint": "0 = never. 1 = a camera focus always aims at a word your prompts keep returning to, not at the shot's own. Ratings nudge the values in between."},
|
||||
"hint": "0 = never. 1 = a camera focus always aims at a word your prompts keep returning to, not at the shot's own. Needs camera moves or reframes on, and a few prompts seen. Ratings nudge the values in between."},
|
||||
"shot_views": {"type": "bool", "default": False, "label": "Shot views",
|
||||
"hint": "Later shots open on a view (side, from above, POV…), never the same twice in a row."},
|
||||
"shot_views_chance": {"type": "float", "default": 0.4, "min": 0.0, "max": 1.0, "step": 0.05, "label": "Chance a shot gets a view",
|
||||
|
||||
@@ -550,12 +550,15 @@ def _own_words():
|
||||
return {w for t in texts for w in re.findall(r"[a-z]+", re.sub(r"\{[a-z]+\}", " ", t.lower()))}
|
||||
|
||||
|
||||
ORDINARY = {"back", "side", "front", "pan", "track", "arc", "hold"}
|
||||
|
||||
|
||||
def _infused(picture, taken, bank, rng):
|
||||
"""-> a candidate for the camera to aim at, drawn from the words the user's prompts keep returning to
|
||||
(`bank`: [(lemma, weight)]), or None. Never a word the shot already names (that would change nothing)
|
||||
and never one of this module's own words."""
|
||||
own = _own_words()
|
||||
mine = lambda lemma: lemma in own or any(lemma + end in own for end in ("s", "es", "d", "ed", "ing")) # "zoom" for "zooms", "fade" for "fades"
|
||||
own = _own_words() - ORDINARY # words that are also plain nouns (a person's back, a frying pan) stay available
|
||||
mine = lambda lemma: lemma not in ORDINARY and (lemma in own or any(lemma + end in own for end in ("s", "es", "d", "ed", "ing"))) # "zoom" for "zooms", "fade" for "fades"
|
||||
options = [(lemma, w) for lemma, w in bank if not mine(lemma) and lemma not in taken and w > 0]
|
||||
if not options:
|
||||
return None
|
||||
@@ -638,7 +641,7 @@ def add_camera_moves(text, seed=None, chance=1.0, prior=None, choices=None, arms
|
||||
# a view and a focus. Never inside one shortcut. Nothing in a prompt knows the video's
|
||||
# length, so the caller hands it in (seconds).
|
||||
STAMP = re.compile(r"\b\d\d:\d\d\.\d{3}\b")
|
||||
ANY_STAMP = re.compile(r"\bAt\s+\d{1,2}:\d{2}(?:\.\d+)?\s*,\s*(?:the\s+)?(?:shot|camera|scene|cut|\w+\s+cut|" + _CUT_VERB + r")\b|\b\d\d:\d\d\.\d{3}\b", re.I)
|
||||
ANY_STAMP = re.compile(r"\bAt\s+\d{1,2}:\d{2}(?:\.\d+)?\s*,\s*(?:(?:the\s+)?(?:shot|camera)\s+(?:\w+ly\s+)?" + _CUT_VERB + r"|(?:\w+\s+)?cut\s+(?:to|away|back|into)|" + _CUT_VERB + r")\b|\b\d\d:\d\d\.\d{3}\b", re.I)
|
||||
# The official MiniMax-H3 guide (docs/VIDEO_PROMPT_WRITING_GUIDE_base_en.md) approves these ordinary cut phrases.
|
||||
# Dissolve, fade and wipe are for when the user asks for them, never default.
|
||||
# Each cut says what is new: a new angle, place, subject, viewpoint, state or moment. Only these four verbs.
|
||||
|
||||
@@ -97,3 +97,13 @@ def test_the_chance_follows_the_slider_at_the_ends_and_ratings_tilt_it_between()
|
||||
def test_a_clock_time_in_the_story_does_not_count_as_a_written_cut_time():
|
||||
assert not engine.ANY_STAMP.search("She arrives at 8:30, and sits.")
|
||||
assert engine.ANY_STAMP.search("At 00:04.000, the shot cuts to her hand.")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("line", ["At 9:30, he cut the rope.", "At 9:30, she cut the cake slowly.", "At 9:30, the scene opens in a park."])
|
||||
def test_a_clock_time_before_ordinary_story_text_is_not_a_written_cut_time(line):
|
||||
assert not engine.ANY_STAMP.search(line)
|
||||
|
||||
|
||||
def test_words_that_are_also_plain_nouns_can_be_infused():
|
||||
got = {engine._infused("", set(), [("back", 1), ("pan", 1)], __import__("random").Random(k))[0] for k in range(40)}
|
||||
assert got == {"back", "pan"}
|
||||
|
||||
Reference in New Issue
Block a user