- Header: view toggles (Assets/Preview/Properties/Auto-hide) move to the top bar; progress, the prompt warning and scene actions (rate, upscale, interpolate) move to the row under the toolbar; the timeline head never wraps from 1000px up and never clips its controls. The top bar grows a line instead of spilling when it wraps. - Kit: every literal pixel size in element CSS is a token (the ratchet test is now zero); Composer's text box follows the viewport; the catalogue shows the component sizes. - test:viewport adds a fake ComfyUI queue and socket for the render-running state and asserts the header is one row. - Auto Montage removed (button, window, code, README/docs, tests). Saved projects holding pieces it made still load: the reused flag is kept. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
164 lines
9.3 KiB
Python
164 lines
9.3 KiB
Python
import pytest
|
||
|
||
from core import config, projects, shortcuts, suggestions
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def _store(tmp_path, monkeypatch):
|
||
monkeypatch.setattr(config, "ROOT", tmp_path)
|
||
monkeypatch.setattr(config, "SHORTCUTS_FILE", tmp_path / "s.json")
|
||
monkeypatch.setattr(config, "SHORTCUT_CATEGORIES_FILE", tmp_path / "c.json")
|
||
monkeypatch.setattr(config, "PROJECTS_DIR", tmp_path / "p")
|
||
|
||
|
||
def _project(*texts):
|
||
p = projects.Project(name="x", scenes=[projects.Scene(text=t) for t in texts])
|
||
projects.save(p)
|
||
|
||
|
||
def test_pairs_follows_counts_come_from_typed_triggers_only():
|
||
for n in ("rain", "neon", "fox"):
|
||
shortcuts.save({"name": n, "triggers": [n], "replacements": ["r"]})
|
||
shortcuts.save({"name": "off", "triggers": ["dog"], "replacements": ["r"], "enabled": False})
|
||
_project("rain and neon here", "", "a fox, dog", "raining") # "raining" is not "rain"
|
||
s = suggestions.stats()
|
||
assert s["scenes"] == 3 and s["counts"] == {"rain": 1, "neon": 1, "fox": 1}
|
||
assert s["pairs"] == [["neon", "rain", 1]] or s["pairs"] == [["rain", "neon", 1]]
|
||
assert sorted(map(tuple, s["follows"])) == [("neon", "fox", 1), ("rain", "fox", 1)]
|
||
|
||
|
||
def test_no_projects_or_no_shortcuts_is_an_empty_answer():
|
||
assert suggestions.stats() == {"scenes": 0, "counts": {}, "pairs": [], "follows": [], "scores": {}, "rated_pairs": [],
|
||
"guess": {"ready": False, "right": 0, "of": 0, "prompts": 0, "weights": {}}}
|
||
|
||
|
||
def test_excluded_scenes_and_sub_clips_are_skipped_and_follows_use_timeline_order():
|
||
for n in ("rain", "neon", "fox"):
|
||
shortcuts.save({"name": n, "triggers": [n], "replacements": ["r"]})
|
||
scenes = [projects.Scene(id="a", text="rain"), projects.Scene(id="b", text="neon"),
|
||
projects.Scene(id="c", text="fox", excluded=True),
|
||
projects.Scene(id="d", text="rain", gen_unit_id="a", cut_offset_frames=24)]
|
||
projects.save(projects.Project(name="x", scenes=scenes, timeline_order=["b", "a", "c", "d"]))
|
||
s = suggestions.stats()
|
||
assert s["scenes"] == 2 and s["counts"] == {"rain": 1, "neon": 1}
|
||
assert s["follows"] == [["neon", "rain", 1]]
|
||
|
||
|
||
def test_reused_copies_are_not_shots_the_user_wrote():
|
||
for n in ("rain", "neon"):
|
||
shortcuts.save({"name": n, "triggers": [n], "replacements": ["r"]})
|
||
scenes = [projects.Scene(id="a", text="rain"), projects.Scene(id="b", text="neon"),
|
||
projects.Scene(id="p1", text="rain", gen_unit_id="p1", reused=True),
|
||
projects.Scene(id="p2", text="neon", gen_unit_id="p2", reused=True)]
|
||
projects.save(projects.Project(name="x", scenes=scenes, timeline_order=["a", "b", "p1", "p2"]))
|
||
s = suggestions.stats()
|
||
assert s["scenes"] == 2 and s["counts"] == {"rain": 1, "neon": 1} and s["follows"] == [["rain", "neon", 1]]
|
||
|
||
|
||
def test_ratings_score_shortcuts_and_pairs_and_a_bad_image_blames_nothing():
|
||
for n in ("rain", "neon", "fox"):
|
||
shortcuts.save({"name": n, "triggers": [n], "replacements": ["r"]})
|
||
projects.save(projects.Project(name="x", scenes=[
|
||
projects.Scene(text="rain neon", rating="10"), projects.Scene(text="rain neon", rating="10|loved"),
|
||
projects.Scene(text="neon fox", rating="Disliked: bad composition"),
|
||
projects.Scene(text="fox", rating="Disliked: bad image"), projects.Scene(text="fox", rating="")]))
|
||
s = suggestions.stats()
|
||
assert s["scores"] == {"rain": 2.25, "neon": 1.125, "fox": 0.5}
|
||
assert sorted(map(tuple, s["rated_pairs"])) == [("fox", "neon", 0.5), ("neon", "rain", 2.25)]
|
||
assert [suggestions.vote(x) for x in ("1", "6", "5", "Disliked: bad image", "odd")] == [-1, 1, -1, 0, 0]
|
||
|
||
|
||
def test_a_rating_counts_for_the_text_it_was_given_to_not_a_rewrite():
|
||
for n in ("rain", "neon"):
|
||
shortcuts.save({"name": n, "triggers": [n], "replacements": ["r"]})
|
||
projects.save(projects.Project(name="x", scenes=[projects.Scene(text="neon", rating="10", rated={"text": "rain", "at": 1})]))
|
||
s = suggestions.stats()
|
||
assert s["scores"] == {"rain": 1.5} and s["counts"] == {"neon": 1}
|
||
assert projects.Scene.from_dict({"rated": {"text": "rain", "at": 1}}).rated == {"text": "rain", "at": 1}
|
||
|
||
|
||
def test_the_miner_finds_exactly_what_the_expander_replaces():
|
||
import random
|
||
words = ["rain", "golden", "hour", "fox", "tail", "neon-lit", "neon", "a", "the", "end", ",", "fox's", "\n", "raining", "GOLDEN"]
|
||
items = [shortcuts.Shortcut(name=f"n{i}", triggers=[w], replacements=[f"<{i}>"])
|
||
for i, w in enumerate(["rain", "golden hour", "golden", "fox", "fox tail", "neon-lit", "a", "the end"])]
|
||
fired, rng = shortcuts.matcher(items), random.Random(7)
|
||
for _ in range(500):
|
||
text = " ".join(rng.choice(words) for _ in range(12))
|
||
out = shortcuts.expand(text, shortcuts=items, seed=1)
|
||
assert fired(text) == {f"n{i}" for i in range(len(items)) if f"<{i}>" in out}, text
|
||
|
||
|
||
def test_ratings_outlive_a_regenerate_a_cleared_text_and_a_left_out_clip():
|
||
for n in ("rain", "neon", "fox"):
|
||
shortcuts.save({"name": n, "triggers": [n], "replacements": ["r"]})
|
||
shown = {"media": {"filename": "3.mp4"}, "promptId": "p3"}
|
||
projects.save(projects.Project(name="x", scenes=[
|
||
projects.Scene(id="a", text="fox", rating=""),
|
||
projects.Scene(id="b", text="", rating="10", rated={"text": "neon"}),
|
||
projects.Scene(id="c", text="fox", rating="10", rated={"text": "fox"}, excluded=True)],
|
||
scene_renders={"a": shown},
|
||
scene_variants={"a": [{"media": {"filename": "1.mp4"}, "promptId": "p1", "rating": "1", "rated": {"text": "rain"}},
|
||
{**shown, "rating": "10", "rated": {"text": "fox"}}]})) # the take on the clip: its head speaks for it
|
||
assert suggestions.stats()["scores"] == {"rain": 0.5, "neon": 1.5, "fox": 1.5}
|
||
|
||
|
||
def test_a_trigger_lower_reshapes_is_still_found_as_the_expander_finds_it():
|
||
items = [shortcuts.Shortcut(name="big", triggers=["İstanbul"], replacements=["<b>"]),
|
||
shortcuts.Shortcut(name="small", triggers=["istanbul"], replacements=["<s>"])]
|
||
out = shortcuts.expand("istanbul", shortcuts=items, seed=1)
|
||
assert shortcuts.matcher(items)("istanbul") == {"big" if "<b>" in out else "small"}
|
||
|
||
|
||
def test_each_dislike_in_a_row_cuts_deeper_a_like_lifts_and_ends_the_run_and_nothing_reaches_zero():
|
||
shortcuts.save({"name": "fox", "triggers": ["fox"], "replacements": ["r"]})
|
||
rate = lambda label, at: projects.Scene(text="fox", rating=label, rated={"text": "fox", "at": at})
|
||
projects.save(projects.Project(name="x", scenes=[rate("1", 4), rate("10", 3), rate("1", 1), rate("1", 2)])) # scene order is not time order
|
||
assert suggestions.stats()["scores"] == {"fox": 0.5 * 0.25 * 1.5 * 0.5}
|
||
assert suggestions.weigh([-1] * 100) > 0 and suggestions.weigh([-1] * 100 + [1]) > suggestions.weigh([-1] * 100)
|
||
|
||
|
||
def test_the_miner_agrees_with_the_expander_on_overlapping_triggers_and_folded_letters():
|
||
cases = [(["a b", "b c b"], "a b c b c b"), (["smile"], "ſmile"), (["istanbul"], "İstanbul"), (["İstanbul", "istanbul"], "istanbul")]
|
||
for triggers, text in cases:
|
||
items = [shortcuts.Shortcut(name=f"n{i}", triggers=[t], replacements=[f"<{i}>"]) for i, t in enumerate(triggers)]
|
||
out = shortcuts.expand(text, shortcuts=items, seed=1)
|
||
assert shortcuts.matcher(items)(text) == {f"n{i}" for i in range(len(items)) if f"<{i}>" in out}, (triggers, text)
|
||
|
||
|
||
def test_an_odd_rating_word_teaches_nothing_and_breaks_nothing():
|
||
assert suggestions.vote("²") == 0 and suggestions.vote("10") == 0
|
||
|
||
|
||
def test_a_rating_given_to_an_empty_prompt_does_not_follow_the_text_written_later():
|
||
shortcuts.save({"name": "neon", "triggers": ["neon"], "replacements": ["r"]})
|
||
projects.save(projects.Project(name="x", scenes=[projects.Scene(text="neon", rating="10", rated={"text": "", "at": 1})]))
|
||
assert suggestions.stats()["scores"] == {}
|
||
|
||
|
||
def test_like_guess_turns_on_only_when_it_calls_held_out_prompts_right():
|
||
import random
|
||
rng = random.Random(1)
|
||
subjects = ["fox", "cat", "owl", "deer", "wolf", "crow", "hare", "seal", "lynx", "mole", "toad", "newt"]
|
||
clear = [(f"a {s} {mood}", 1 if mood == "neon" else -1) for s in subjects for mood in ("neon", "fog") for _ in range(2)]
|
||
g = suggestions.guess(clear)
|
||
assert g["ready"] and g["prompts"] == 24 and g["weights"]["neon"] > 0 > g["weights"]["fog"]
|
||
assert abs(g["weights"].get("fox", 0)) < g["weights"]["neon"]
|
||
# One prompt rated over and over: seeds, not words. Never on, however many.
|
||
assert not suggestions.guess([("a fox, neon", rng.choice((1, -1))) for _ in range(60)])["ready"]
|
||
# Votes that ignore the words: off (no weights sent either).
|
||
for seed in range(200):
|
||
r = random.Random(seed)
|
||
noise = suggestions.guess([(f"a {s} {m}", r.choice((1, -1))) for s in subjects for m in ("neon", "fog", "sun")])
|
||
assert not noise["ready"] and noise["weights"] == {}, seed
|
||
assert not suggestions.guess(clear[:8])["ready"] # too few prompts to tell
|
||
assert suggestions.guess([])["of"] == 0
|
||
|
||
|
||
def test_stats_feeds_the_like_guess_the_rated_text():
|
||
p = projects.Project(name="x", scenes=[projects.Scene(text="now", rating="10", rated={"text": "then", "at": 1}),
|
||
projects.Scene(text="b", rating="Disliked: bad image")])
|
||
projects.save(p)
|
||
g = suggestions.stats()["guess"]
|
||
assert (g["of"], g["prompts"]) == (1, 1)
|