fix: more forms of a user-written cut or transition are kept without a second opener; cut times written anywhere leave the prompt alone; the status line names the transitions used; tests for the bin badge repaint
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
848cf2e041
commit
0d281c5a6e
@@ -48,3 +48,18 @@ test("the selected scene's references are marked on their tiles, and the menu ta
|
||||
clear.click();
|
||||
assert.deepEqual(set, [["s1", "references", ["m1"]], ["s1", "references", []]]);
|
||||
});
|
||||
|
||||
test("a changed pin repaints the slot numbers on the tiles even when the list of references is the same", async () => {
|
||||
const fixed = { m1: 2 };
|
||||
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");
|
||||
let fire;
|
||||
hub.setup({ host, app: { project, api, mediaPeek: {}, say: () => {}, on: (cb) => { fire = cb; return () => {}; } } });
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
const flag = (n) => [...[...host.querySelectorAll(".cx-cell")].find((x) => x.textContent.includes(n)).querySelectorAll(".cx-cell-flag")].map((x) => x.textContent).join();
|
||||
assert.deepEqual([flag("cat.png"), flag("dog.png")], ["R2", "R1"], "pinned at 2, the other takes the free number");
|
||||
delete fixed.m1;
|
||||
fire("project");
|
||||
assert.deepEqual([flag("cat.png"), flag("dog.png")], ["R1", "R2"], "unpinned: back to list order");
|
||||
});
|
||||
|
||||
@@ -25,7 +25,11 @@ AUDIO = re.compile(r"\b(music|soundtrack|score|melody|instrumental|beat|bassline
|
||||
# H3's own vocabulary (VIDEO_PROMPT_WRITING_GUIDE_base_en.md): a shot opening "the camera cuts
|
||||
# to" is a cut, not a camera move, and must not make the shot look like it already has one.
|
||||
# Every cut or transition verb this module writes or a user is likely to write after "the shot" / "the camera".
|
||||
_CUT_VERB = r"(?:cuts|transitions|changes|switches|cross-dissolves|dissolves|wipes|fades)(?:\s+(?:out|across))?"
|
||||
_CUT_VERB = r"(?:cuts|transitions|changes|switches|cross-dissolves|cross-fades|dissolves|wipes|fades)(?:\s+(?:out|across))?"
|
||||
# A cut or transition a user may open a shot with: "The shot slowly fades to", "Then the camera cuts to", "Smash cut to", "Fade to black".
|
||||
_LEAD_PHRASE = (r"(?:(?:then|suddenly|next)[,\s]+)?(?:(?:the\s+)?(?:camera|shot|scene)\s+(?:(?:then|slowly|quickly|suddenly)\s+)?" + _CUT_VERB
|
||||
+ r"|(?:an?\s+)?(?:smash\s+cut|hard\s+cut|match\s+cut|jump\s+cut|cut|cross-?dissolve|dissolve|cross-?fade|fade|wipe)(?:\s+(?:out|in|through\s+black))?)"
|
||||
r"\s+(?:to|into|in\s+on)\b")
|
||||
CUT = re.compile(r"\b(?:the\s+)?(?:camera|shot)\s+" + _CUT_VERB + r"\s+to\b", re.I)
|
||||
# Whether a shot ALREADY has a camera move. Words like tilt, roll, pull, push, arc, focus are
|
||||
# ordinary body movement ("tilts <Subject 1>'s head up and down", "pulls out a knife"), and
|
||||
@@ -603,6 +607,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*,|\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.
|
||||
@@ -621,7 +626,7 @@ CUT_OPENERS = ("the camera cuts to a new angle", "the camera cuts to the next mo
|
||||
"the shot changes to a new view", "the shot changes to what comes next",
|
||||
"the shot switches to what follows", "the shot switches to the next moment")
|
||||
OUR_OPENERS = CUT_OPENERS + tuple(phrase for _, phrase in TRANSITIONS) # everything this module writes after a cut time
|
||||
LEAD_CUT = re.compile(r"^\s*(?:the\s+)?(?:camera|shot)\s+" + _CUT_VERB + r"\s+to\b", re.I)
|
||||
LEAD_CUT = re.compile(r"^\s*" + _LEAD_PHRASE, re.I)
|
||||
MIN_SHOT_SECONDS = 2
|
||||
|
||||
|
||||
@@ -685,7 +690,7 @@ def add_shot_cuts(text, seconds, seed=0, chance=0.5, pieces=(), transition=0.0):
|
||||
return text, info
|
||||
bodies = [text[m.end():(marks[i + 1].start() if i + 1 < len(marks) else len(text))]
|
||||
for i, m in enumerate(marks)]
|
||||
if any(STAMP.search(b[:60]) for b in bodies[1:]):
|
||||
if any(ANY_STAMP.search(b) for b in bodies):
|
||||
info["why"] = "cut times are already written, left as they are"
|
||||
return text, info
|
||||
header = text[:marks[0].start()]
|
||||
@@ -726,7 +731,7 @@ def add_shot_cuts(text, seconds, seed=0, chance=0.5, pieces=(), transition=0.0):
|
||||
info["arms"] = []
|
||||
return text, info
|
||||
out = [header]
|
||||
last = None # the transition kind of the cut before this one
|
||||
last, kinds = None, [] # the transition kind of the cut before this one, and every kind used
|
||||
for k, (picture, sound) in enumerate(blocks):
|
||||
label = f"[Shot {k + 1}]"
|
||||
body = picture
|
||||
@@ -738,11 +743,13 @@ def add_shot_cuts(text, seconds, seed=0, chance=0.5, pieces=(), transition=0.0):
|
||||
last = None
|
||||
else:
|
||||
opener, last = _opener(rng, transition, last)
|
||||
if last:
|
||||
kinds.append(last)
|
||||
body = " " + stamp + opener + ". " + body.lstrip()
|
||||
else:
|
||||
body = body if body.startswith(" ") else " " + body
|
||||
out.append(label + body + sound)
|
||||
info.update(after=len(blocks), times=[_stamp(t) for t in times])
|
||||
info.update(after=len(blocks), times=[_stamp(t) for t in times], transitions=kinds)
|
||||
return "".join(out), info
|
||||
|
||||
|
||||
@@ -983,7 +990,7 @@ def own_words_removed(picture):
|
||||
|
||||
_OUR_OPENER = re.compile(r"^\s*At \d\d:\d\d\.\d{3},\s*(?:" + "|".join(re.escape(o) for o in OUR_OPENERS)
|
||||
+ r")\.\s*", re.I)
|
||||
_CUT_WORDS = re.compile(r"^\s*(?:At \d\d:\d\d\.\d{3},\s*)?(?:the\s+)?(?:camera|shot)\s+" + _CUT_VERB + r"\s+to\b\s*", re.I)
|
||||
_CUT_WORDS = re.compile(r"^\s*(?:At \d\d:\d\d\.\d{3},\s*)?" + _LEAD_PHRASE + r"\s*", re.I)
|
||||
|
||||
|
||||
def content_fingerprint(text):
|
||||
@@ -993,7 +1000,7 @@ def content_fingerprint(text):
|
||||
body = re.sub(r"\[\s*Shot\s+\d+\s*\]", " ", text or "", flags=re.I)
|
||||
body = re.sub(r"At \d\d:\d\d\.\d{3},\s*(?:" + "|".join(re.escape(o) for o in OUR_OPENERS) + r")\.",
|
||||
" ", body, flags=re.I)
|
||||
body = re.sub(r"At \d\d:\d\d\.\d{3},\s*(?=(?:the\s+)?(?:camera|shot)\s+" + _CUT_VERB + r"\s+to\b)", " ", body, flags=re.I) # the time before a cut phrase the user wrote
|
||||
body = re.sub(r"At \d\d:\d\d\.\d{3},\s*(?=" + _LEAD_PHRASE + r")", " ", body, flags=re.I) # the time before a cut phrase the user wrote
|
||||
body = _REFRAME.sub(" ", body)
|
||||
body = re.sub(r"\s+", " ", body)
|
||||
body = re.sub(r"(?:^|(?<=[.!?]\s))(?:" + "|".join(re.escape(v) for v in VIEWS) + r")\.", " ", body)
|
||||
|
||||
@@ -53,6 +53,8 @@ def rewrite(text, values, seconds=None, pieces=None):
|
||||
transition=float(values.get("cut_difference_chance", 0.0)))
|
||||
arms += info.get("arms", [])
|
||||
said.append(f"cuts at {', '.join(info['times'])}" + (f" ({info['before']}→{info['after']} shots)" if info["after"] != info["before"] else "") if info["times"] else f"cuts: {info['why'] or 'none'}")
|
||||
if info.get("transitions"):
|
||||
said[-1] += f" · transitions: {', '.join(info['transitions'])}"
|
||||
if values.get("reframe_same_shot") and engine.SHOT.search(text or ""):
|
||||
text, info = engine.add_reframes(text, seed=seed, chance=memory.reframe_chance(float(values.get("reframe_chance", 0.5))),
|
||||
pieces=_pieces() if pieces is None else pieces, stats=memory.view_stats(),
|
||||
|
||||
@@ -86,7 +86,12 @@ import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("phrase", ["The shot fades to black.", "The shot fades out to black.", "The shot dissolves to her hand.",
|
||||
"The camera wipes across to her hand.", "The shot cross-dissolves to her hand."])
|
||||
"The camera wipes across to her hand.", "The shot cross-dissolves to her hand.",
|
||||
"Fade to black.", "Cut to her hand.", "Dissolve to her hand.", "A cross-dissolve to her hand.",
|
||||
"Smash cut to her hand.", "The shot cross-fades to her hand.", "Then the camera cuts to her hand.",
|
||||
"The camera then cuts to her hand.", "The shot slowly fades to black.",
|
||||
"The shot quickly wipes across to her hand.", "The shot dissolves into her hand.",
|
||||
"The scene fades to black."])
|
||||
def test_the_cut_phrases_the_hint_names_are_kept_without_a_second_opener(monkeypatch, tmp_path, phrase):
|
||||
monkeypatch.setenv("SHOT_CAMERA_MEMORY", str(tmp_path / "m.json"))
|
||||
out, _ = engine.add_shot_cuts(f"[Shot 1] {DANCE} [Shot 2] {phrase} {WAVE}", 10, seed=1, chance=0, pieces=[DANCE, WAVE], transition=1.0)
|
||||
@@ -115,3 +120,21 @@ def test_each_cut_rolls_for_itself_so_a_shot_can_mix_plain_and_transition_cuts(m
|
||||
plain = sum(out.count(o) for o in engine.CUT_OPENERS)
|
||||
mixes += plain > 0 and sum(out.count(p) for _, p in engine.TRANSITIONS) > 0
|
||||
assert mixes > 5
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stamp", ["At 00:04.000, the camera cuts to the door.", "At 00:03, the camera cuts to her hand.",
|
||||
"At 0:03.5, the shot cuts to her hand."])
|
||||
def test_cut_times_the_user_wrote_anywhere_leave_the_prompt_alone(monkeypatch, tmp_path, stamp):
|
||||
monkeypatch.setenv("SHOT_CAMERA_MEMORY", str(tmp_path / "m.json"))
|
||||
text = f"[Shot 1] {DANCE} [Shot 2] {WAVE} " + "and she keeps going " * 6 + stamp
|
||||
out, info = engine.add_shot_cuts(text, 12, seed=1, chance=1, pieces=[DANCE, WAVE], transition=1.0)
|
||||
assert out == text and "already written" in info["why"], stamp
|
||||
|
||||
|
||||
def test_the_status_line_says_which_transitions_were_used(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("SHOT_CAMERA_MEMORY", str(tmp_path / "m.json"))
|
||||
said = nodes.rewrite(f"[Shot 1] {DANCE} {WAVE} {DANCE}", {"cut_same_shot": True, "shot_cuts_chance": 1, "cut_difference_chance": 1},
|
||||
seconds=12, pieces=[DANCE, WAVE])[1]
|
||||
assert "transitions: " in said and any(k in said for k in ("dissolve", "fade", "wipe")), said
|
||||
plain = nodes.rewrite(f"[Shot 1] {DANCE} {WAVE} {DANCE}", {"cut_same_shot": True, "shot_cuts_chance": 1}, seconds=12, pieces=[DANCE, WAVE])[1]
|
||||
assert "transitions" not in plain
|
||||
|
||||
Reference in New Issue
Block a user