Spaces:
Runtime error
Runtime error
Download backend/tests/test_slides.py from firebird-technologies/NoteStack-Test: direct link, hf CLI and curl.
- Browser
- Download file 34.7 kB
-
https://huggingface.co/spaces/firebird-technologies/NoteStack-Test/resolve/main/backend/tests/test_slides.py
- Command line
-
hf download hf://spaces/firebird-technologies/NoteStack-Test/backend/tests/test_slides.py
-
curl -L -o test_slides.py https://huggingface.co/spaces/firebird-technologies/NoteStack-Test/resolve/main/backend/tests/test_slides.py
34.7 kB
| """Slide decks: what the model writes is clamped to a fixed shape, laid out by code in either theme, checked against | |
| the sources, and drawn the same way as pages, a PDF and an editable PowerPoint. The LLM is faked at app.llm.run.""" | |
| import copy | |
| import io | |
| import re | |
| import pytest | |
| from pptx import Presentation | |
| from app.infographics.image import chrome | |
| from app.slides import export | |
| from app.slides.build import finish, view | |
| from app.slides.content import LIMITS, clamp_deck, clamp_slide | |
| from app.slides.layout import GEOMETRY, VARIANTS, assign_variants, compose | |
| from app.slides.render import deck_print_html, slide_html | |
| from tests.test_buildout import _notebook_with_pricing, auth, feed, llm # noqa: F401 (fixtures) | |
| LONG = "Words that keep going and going " * 40 | |
| FULL = { | |
| "title": {"heading": "Pricing that readers say yes to", "kicker": "From 4 posts", "lead": "What two years taught us."}, | |
| "agenda": {"heading": "What we will cover", "points": [{"text": f"Part {i}"} for i in range(9)]}, | |
| "section": {"heading": "The numbers behind trials", "kicker": "Part two", "lead": "Short trials converted twice as well."}, | |
| "points": {"heading": "Why tiers work", "kicker": "Tiers", "lead": "Readers self-select.", | |
| "points": [{"term": "Anchoring", "text": "A higher tier makes the middle one feel fair."}] * 4}, | |
| "two_column": {"heading": "Monthly or annual", "left": {"label": "Monthly", "items": ["Low commitment", "Easy to leave"]}, | |
| "right": {"label": "Annual", "items": ["Two months free", "Lower churn"]}}, | |
| "stat": {"heading": "Short trials win", "stat": {"value": "2x", "label": "more paid signups"}, "lead": "A decision point."}, | |
| "quote": {"heading": "In their words", "quote": {"text": "Thursday would feel empty without it.", "by": "A reader"}}, | |
| "closing": {"heading": "Key takeaways", "takeaways": ["One.", "Two.", "Three."], "closing": "Price the habit."}, | |
| } | |
| def _stuff(slide: dict, text: str) -> dict: | |
| """The same slide with every text field set to `text` (the longest anything could be).""" | |
| s = copy.deepcopy(slide) | |
| for k in ("heading", "kicker", "lead", "closing"): | |
| if k in s: | |
| s[k] = text | |
| for p in s.get("points") or []: | |
| p["text"], p["term"] = text, text[:60] | |
| for side in ("left", "right"): | |
| if side in s: | |
| s[side] = {"label": text, "items": [text] * 6} | |
| if "stat" in s: | |
| s["stat"] = {"value": "123456789012345678", "label": text} | |
| if "quote" in s: | |
| s["quote"] = {"text": text, "by": text} | |
| if "takeaways" in s: | |
| s["takeaways"] = [text] * 5 | |
| return s | |
| def test_every_variant_draws_inside_its_boxes_at_any_length(theme, fmt): | |
| for layout, variants in VARIANTS.items(): | |
| for v in variants: | |
| for text in ("Short text here", LONG, "長い日本語のテキスト" * 30, "Supercalifragilistic" * 12): | |
| raw = {**_stuff(FULL[layout], text), "layout": layout, "variant": v} | |
| deck = clamp_deck({"title": "Deck", "slides": [raw, raw]}, fmt) | |
| for i in range(2): | |
| c = compose(deck, i, theme, fmt, seed=7) | |
| for e in c.elements: | |
| assert 0 <= e.x and e.x + e.w <= 1920 + 1 and 0 <= e.y and e.y + e.h <= 1080 + 1, (layout, v, e.role) | |
| if e.kind == "text": | |
| assert e.text_height() <= e.h + 1, (layout, v, e.role, text[:20]) | |
| def test_clamp_never_lets_bad_input_through(): | |
| garbage = [None, 5, "text", [], {"layout": "nonsense", "heading": "<script>alert(1)</script>**Bold** [1]"}, | |
| {"layout": "stat", "heading": "No number", "stat": {"value": "", "label": "x"}}, | |
| {"layout": "quote", "heading": "No quote", "quote": None, "points": [{"text": "a"}, {"text": "b"}]}, | |
| {"layout": "two_column", "heading": "One side", "left": {"items": ["x"]}, "right": None}, | |
| {"layout": "points", "heading": "Nul\x00 byte— dash", "points": "not a list"}, | |
| {"layout": "closing", "heading": "", "takeaways": None, "closing": None}, | |
| {"layout": "points", "heading": "x" * 5000, "points": [{"term": None, "text": {"nested": 1}}] * 30}] | |
| deck = clamp_deck({"title": None, "slides": garbage}, "detailed") | |
| assert deck and deck["title"] | |
| keys = set(clamp_slide({"heading": "x"}, "detailed")) | |
| for s in deck["slides"]: | |
| assert set(s) == keys # every slide has every key | |
| for value in (s["heading"], s["lead"], s["kicker"]): | |
| assert "<" not in value and "\x00" not in value and "—" not in value and "**" not in value | |
| assert len(s["heading"]) <= LIMITS["detailed"]["heading"] | |
| layouts = [s["layout"] for s in deck["slides"]] | |
| assert layouts[0] == "points" or layouts[0] == "section" # unknown layout becomes a simpler one | |
| assert "stat" not in layouts and "quote" not in layouts and "two_column" not in layouts | |
| assert clamp_deck({"slides": []}, "detailed") is None and clamp_deck("nope", "presenter") is None | |
| def test_pages_escape_everything_the_model_wrote(): | |
| raw = {"layout": "points", "heading": "Fine <b onmouseover=x>heading</b>", "lead": "a & b < c", | |
| "points": [{"term": "<img src=x>", "text": "\"quoted\" <script>x</script> text"}] * 3} | |
| deck = clamp_deck({"title": "<title>", "slides": [raw]}, "detailed") | |
| deck["slides"][0]["variant"] = "a" | |
| html = slide_html(deck, 0, "dark-space", "detailed", 1) | |
| assert "<script" not in html and "<img" not in html and "onmouseover" not in html | |
| assert "a & b < c" in html | |
| def test_variants_never_repeat_in_a_row_and_differ_between_decks(): | |
| slides = [clamp_slide({**FULL[k], "layout": k}, "detailed") for k in | |
| ("title", "agenda", "points", "points", "points", "stat", "points", "quote", "points", "closing")] | |
| a, b = copy.deepcopy(slides), copy.deepcopy(slides) | |
| assign_variants(a, "dark-space", seed=1) | |
| pairs = [(s["layout"], s["variant"]) for s in a] | |
| assert all(pairs[i] != pairs[i + 1] for i in range(len(pairs) - 1)) | |
| points = [s["variant"] for s in a if s["layout"] == "points"] | |
| assert len(set(points)) == len(VARIANTS["points"]) # every points variant used before any repeats | |
| assign_variants(b, "dark-space", seed=2, previous=pairs) | |
| assert [s["variant"] for s in b] != [s["variant"] for s in a] | |
| assert b[0]["variant"] != a[0]["variant"] and b[-1]["variant"] != a[-1]["variant"] # new opener and closer | |
| again = copy.deepcopy(slides) | |
| assign_variants(again, "dark-space", seed=1) | |
| assert [s["variant"] for s in again] == [s["variant"] for s in a] # one deck always draws the same | |
| def test_the_two_themes_compose_differently(): | |
| for layout, variants in VARIANTS.items(): | |
| for v in variants: | |
| assert GEOMETRY["dark-space"][layout][v] != GEOMETRY["light-space"][layout][v] or layout in ("points", "two_column"), \ | |
| (layout, v) | |
| def _deck(fmt: str = "detailed") -> dict: | |
| src = [{"path": "p.md", "line_start": 1, "line_end": 2, "title": "On Pricing"}] | |
| slides = [{**FULL[k], "layout": k, "notes": f"Say {k}", "sources": src} | |
| for k in ("title", "agenda", "points", "two_column", "stat", "quote", "closing")] | |
| deck = clamp_deck({"title": "Pricing", "slides": slides}, fmt) | |
| assign_variants(deck["slides"], "light-space", seed=3) | |
| return deck | |
| def test_powerpoint_has_every_slide_as_editable_text_with_notes(): | |
| deck = _deck() | |
| data = export.build_pptx(deck, "light-space", "detailed", 3) | |
| prs = Presentation(io.BytesIO(data)) | |
| assert len(prs.slides) == len(deck["slides"]) and prs.slide_width == 12192000 | |
| texts = [" ".join(sh.text_frame.text for sh in s.shapes if sh.has_text_frame) for s in prs.slides] | |
| assert "Why tiers work" in texts[2] and "Anchoring" in texts[2] and "middle one feel fair" in texts[2] | |
| notes = prs.slides[2].notes_slide.notes_text_frame.text | |
| assert "Say points" in notes and "On Pricing (lines 1-2)" in notes | |
| def test_pdf_has_one_page_per_slide_and_nothing_overflows(): | |
| from pypdf import PdfReader | |
| deck = finish(_deck(), "dark-space", "detailed", 3) | |
| assert export.overflowing(deck, "dark-space", "detailed", 3) == set() | |
| pdf = PdfReader(io.BytesIO(export.render_pdf(deck, "dark-space", "detailed", 3))) | |
| assert len(pdf.pages) == len(deck["slides"]) | |
| assert round(float(pdf.pages[0].mediabox.width) / float(pdf.pages[0].mediabox.height), 2) == round(1920 / 1080, 2) | |
| def test_a_stored_deck_with_a_broken_slide_still_shows(): | |
| content = {"theme": "dark-space", "format": "detailed", "seed": "abc", "deck": _deck()} | |
| content["deck"]["slides"][2]["variant"] = "zz" # unknown: drawn with the first variant | |
| content["deck"]["slides"][3]["points"] = None | |
| pages = view(content) | |
| assert len(pages) == len(content["deck"]["slides"]) and all(p.startswith("<!doctype html>") for p in pages) | |
| assert view({"deck": {"slides": []}}) == [] and view({}) == [] | |
| assert "Pricing" in deck_print_html(clamp_deck(content["deck"], "detailed"), "x", "detailed", 1) | |
| # --- the whole flow, through the API ------------------------------------------------------------------------------ | |
| def no_chrome(monkeypatch): | |
| """Generation measures slides in Chrome when one is installed; the API tests run without it.""" | |
| monkeypatch.setattr(export, "chrome", lambda: None) | |
| def _fake_many(monkeypatch, pricing_path: str, log: dict): | |
| from app.llm import run | |
| def predict_many(signature, inputs_list, *, db, workspace_id, lm=None, workers=5): | |
| name = signature.__name__ | |
| log.setdefault(name, []).extend(inputs_list) | |
| out = [] | |
| for inp in inputs_list: | |
| if name == "WriteSlide": | |
| good = {"path": pricing_path, "line_start": 1, "line_end": 12} | |
| fake = {"path": "sources/made-up.md", "line_start": 1, "line_end": 3} | |
| slide = {"kicker": "Pricing", "heading": inp["heading"], "lead": "Charging more made readers take it seriously.", | |
| "points": [{"term": "Price", "text": "The paid tier doubled after the price went up."}, | |
| {"term": "Invented", "text": "Ninety percent of readers loved it."}, | |
| {"term": "Seriousness", "text": "Readers took the work seriously."}], | |
| "notes": "Talk about pricing.", "sources": [good, fake]} | |
| if inp["layout"] == "stat": | |
| slide["stat"] = {"value": "90%", "label": "of readers upgraded"} # not in the post: removed | |
| if inp["layout"] == "quote": | |
| slide["quote"] = {"text": "Charging more made my readers take the work seriously.", "by": "Ada"} | |
| if inp["layout"] == "two_column": | |
| slide["left"] = {"label": "Before", "items": ["Low price", "Few paid readers"]} | |
| slide["right"] = {"label": "After", "items": ["Ten dollars", "Paid tier doubled"]} | |
| out.append({"slide": slide}) | |
| elif name == "CheckSlide": | |
| bad = [c for c in inp["claims"] if "Ninety percent" in c] | |
| out.append({"checks": [{"n": int(c.split(".")[0]), "supported": False, "fix": ""} for c in bad]}) | |
| else: | |
| out.append(None) | |
| return out | |
| monkeypatch.setattr(run, "predict_many", predict_many) | |
| def _outline(n: int, layouts=("points", "quote", "stat", "points", "section", "points", "points", "two_column", "points")): | |
| def make(**inputs): | |
| return {"outline": {"title": "Pricing for writers", "subtitle": "Why charging more worked", | |
| "opening_kicker": "Pricing", "agenda_heading": "What we cover", | |
| "slides": [{"layout": layouts[i % len(layouts)], "heading": f"Slide idea {i}", | |
| "purpose": "Make a point", "sources": []} for i in range(n)]}} | |
| return make | |
| CLOSING = {"closing": {"kicker": "Conclusion", "heading": "Key takeaways", | |
| "takeaways": ["Charge more.", "Readers take it seriously.", "The paid tier grows."], | |
| "closing": "Price the work like it matters.", "notes": "Close."}} | |
| def test_short_detailed_deck_is_grounded_checked_and_downloadable(client, auth, run_jobs, feed, llm, monkeypatch, no_chrome): # noqa: F811 | |
| pricing, nb = _notebook_with_pricing(client, auth, run_jobs) | |
| log: dict = {} | |
| _fake_many(monkeypatch, pricing["path"], log) | |
| llm["OutlineDeck"] = _outline(9) # too many for a short deck: trimmed | |
| llm["WriteClosing"] = CLOSING | |
| art = client.post("/api/artifacts/generate", json={"type": "slide_deck", "notebook_id": nb["id"], | |
| "document_ids": [pricing["id"]], "deck_format": "detailed", "deck_length": "short", | |
| "theme": "light-space", "instructions": "For new writers"}, headers=auth).json() | |
| assert art["type_label"] == "Slide deck" | |
| run_jobs() | |
| done = client.get(f"/api/artifacts/{art['id']}", headers=auth).json() | |
| assert done["status"] == "ready", done["content"].get("error") | |
| c = done["content"] | |
| slides = c["deck"]["slides"] | |
| assert 6 <= len(slides) <= 8 and len(done["content"]["slides_html"]) == len(slides) | |
| assert slides[0]["layout"] == "title" and slides[-1]["layout"] == "closing" | |
| assert slides[-1]["closing"] == "Price the work like it matters." and len(slides[-1]["takeaways"]) == 3 | |
| assert not any(s["layout"] == "agenda" for s in slides) # short decks have no agenda | |
| assert c["theme"] == "light-space" and c["format"] == "detailed" | |
| body = [s for s in slides[1:-1]] | |
| for s in body: | |
| assert all(src["path"] == pricing["path"] for src in s["sources"]) # the invented path is gone | |
| assert all("Ninety percent" not in p["text"] for p in s["points"]) # unsupported claim removed | |
| assert s["layout"] != "stat" # its number was not in the post | |
| assert any(s["layout"] == "quote" for s in body) # the quote is in the post word for word | |
| pairs = [(s["layout"], s["variant"]) for s in slides] | |
| assert all(pairs[i] != pairs[i + 1] for i in range(len(pairs) - 1)) | |
| assert all(log["WriteSlide"][0]["source_lines"]) and log["WriteSlide"][0]["deck_format"] == "detailed" | |
| html = "".join(c["slides_html"]) | |
| assert "<script" not in html and "Pricing for writers" in html | |
| pptx = client.get(f"/api/artifacts/{art['id']}/slides.pptx", headers=auth) | |
| assert pptx.status_code == 200 and "Pricing for writers.pptx" in pptx.headers["content-disposition"] | |
| assert len(Presentation(io.BytesIO(pptx.content)).slides) == len(slides) | |
| assert client.get(f"/api/artifacts/{art['id']}/slides.pdf", headers=auth).status_code == 503 # no Chrome here | |
| assert client.get(f"/api/artifacts/{art['id']}/slides.doc", headers=auth).status_code == 422 | |
| def test_default_presenter_deck_has_an_agenda_and_stays_in_range(client, auth, run_jobs, feed, llm, monkeypatch, no_chrome): # noqa: F811 | |
| pricing, nb = _notebook_with_pricing(client, auth, run_jobs) | |
| log: dict = {} | |
| _fake_many(monkeypatch, pricing["path"], log) | |
| llm["OutlineDeck"] = _outline(7) | |
| llm["WriteClosing"] = CLOSING | |
| art = client.post("/api/artifacts/generate", json={"type": "slide_deck", "notebook_id": nb["id"], | |
| "document_ids": [pricing["id"]], "deck_format": "presenter", "deck_length": "default", | |
| "language": "Spanish"}, headers=auth).json() | |
| run_jobs() | |
| done = client.get(f"/api/artifacts/{art['id']}", headers=auth).json() | |
| slides = done["content"]["deck"]["slides"] | |
| assert done["status"] == "ready" and 9 <= len(slides) <= 11 | |
| assert slides[1]["layout"] == "agenda" and [p["text"] for p in slides[1]["points"]][:2] == ["Slide idea 0", "Slide idea 1"] | |
| assert done["content"]["theme"] == "dark-space" # the default | |
| assert log["WriteSlide"][0]["language"] == "Spanish" and log["WriteSlide"][0]["deck_format"] == "presenter" | |
| for s in slides: | |
| assert all(len(p["text"]) <= LIMITS["presenter"]["text"] for p in s["points"] if s["layout"] != "agenda") | |
| def test_too_little_material_fails_cleanly_and_sources_are_required(client, auth, run_jobs, feed, llm, monkeypatch, no_chrome): # noqa: F811 | |
| pricing, nb = _notebook_with_pricing(client, auth, run_jobs) | |
| _fake_many(monkeypatch, pricing["path"], {}) | |
| llm["OutlineDeck"] = _outline(1) | |
| art = client.post("/api/artifacts/generate", json={"type": "slide_deck", "notebook_id": nb["id"], | |
| "document_ids": [pricing["id"]], "deck_length": "default"}, headers=auth).json() | |
| run_jobs() | |
| failed = client.get(f"/api/artifacts/{art['id']}", headers=auth).json() | |
| assert failed["status"] == "failed" and "Short" in failed["content"]["error"] | |
| assert llm["_calls"].count("OutlineDeck") >= 2 # asked again before giving up | |
| assert client.post("/api/artifacts/generate", json={"type": "slide_deck", "notebook_id": nb["id"], | |
| "document_ids": []}, headers=auth).status_code == 400 | |
| assert client.get(f"/api/artifacts/{art['id']}/slides.pptx", headers=auth).status_code == 404 | |
| def test_a_slide_the_model_fails_to_write_is_written_from_its_sources(client, auth, run_jobs, feed, llm, monkeypatch, no_chrome): # noqa: F811 | |
| from app.llm import run | |
| pricing, nb = _notebook_with_pricing(client, auth, run_jobs) | |
| log: dict = {} | |
| _fake_many(monkeypatch, pricing["path"], log) | |
| real = run.predict_many | |
| def flaky(signature, inputs_list, **kw): | |
| out = real(signature, inputs_list, **kw) | |
| return [None] * len(out) if signature.__name__ == "WriteSlide" else out | |
| monkeypatch.setattr(run, "predict_many", flaky) | |
| llm["OutlineDeck"] = lambda **kw: {"outline": {"title": "Pricing", "subtitle": "", "opening_kicker": "", "agenda_heading": "", | |
| "slides": [{"layout": "points", "heading": f"Idea {i}", "purpose": "A point", | |
| "sources": [{"path": pricing["path"], "line_start": 1, "line_end": 14}]} | |
| for i in range(4)]}} | |
| llm["WriteClosing"] = lambda **kw: (_ for _ in ()).throw(RuntimeError("model down")) | |
| art = client.post("/api/artifacts/generate", json={"type": "slide_deck", "notebook_id": nb["id"], | |
| "document_ids": [pricing["id"]], "deck_length": "short"}, headers=auth).json() | |
| run_jobs() | |
| done = client.get(f"/api/artifacts/{art['id']}", headers=auth).json() | |
| assert done["status"] == "ready", done["content"].get("error") | |
| slides = done["content"]["deck"]["slides"] | |
| assert [s["heading"] for s in slides[1:-1]] == [f"Idea {i}" for i in range(4)] | |
| assert slides[-1]["layout"] == "closing" and slides[-1]["takeaways"] | |
| assert len(log["WriteSlide"]) == 8 # each slide tried twice | |
| assert re.search(r"Charging more|ten dollars|seriously", " ".join(str(s) for s in slides[1:-1])) | |
| def test_the_opening_and_closing_say_only_what_the_slides_say(client, auth, run_jobs, feed, llm, monkeypatch, no_chrome): # noqa: F811 | |
| pricing, nb = _notebook_with_pricing(client, auth, run_jobs) | |
| _fake_many(monkeypatch, pricing["path"], {}) | |
| outline = _outline(4) | |
| def mixed_up(**kw): | |
| out = outline(**kw) | |
| out["outline"]["subtitle"] = "Ada won a writing award for her pricing" # joins two things no slide says | |
| return out | |
| llm["OutlineDeck"] = mixed_up | |
| takeaways = ["Charge more.", "Ada sold a million copies.", "The paid tier grows."] | |
| llm["WriteClosing"] = {"closing": {**CLOSING["closing"], "takeaways": takeaways}} | |
| def check(**kw): | |
| claims = kw["claims"] | |
| return {"checks": [{"n": int(c.split(".")[0]), "supported": False, "fix": ""} for c in claims | |
| if "award" in c or "million" in c]} | |
| llm["CheckSlide"] = check | |
| art = client.post("/api/artifacts/generate", json={"type": "slide_deck", "notebook_id": nb["id"], | |
| "document_ids": [pricing["id"]], "deck_length": "short"}, headers=auth).json() | |
| run_jobs() | |
| slides = client.get(f"/api/artifacts/{art['id']}", headers=auth).json()["content"]["deck"]["slides"] | |
| assert "award" not in slides[0]["lead"] and slides[0]["heading"] == "Pricing for writers" | |
| assert slides[-1]["takeaways"] == ["Charge more.", "The paid tier grows."] | |
| def test_long_deck_has_parts_and_the_request_reaches_every_step(client, auth, run_jobs, feed, llm, monkeypatch, no_chrome): # noqa: F811 | |
| pricing, nb = _notebook_with_pricing(client, auth, run_jobs) | |
| log: dict = {} | |
| _fake_many(monkeypatch, pricing["path"], log) | |
| layouts = ("section", "points", "stat", "points", "section", "quote", "points", "two_column", "section", "points", | |
| "points", "points") | |
| llm["OutlineDeck"] = _outline(12, layouts) | |
| llm["CheckRequest"] = {"asks": [{"ask": "For new writers", "covered": True}]} | |
| seen: dict = {} | |
| def closing(**kw): | |
| seen["closing"] = kw["request"] | |
| return CLOSING | |
| llm["WriteClosing"] = closing | |
| art = client.post("/api/artifacts/generate", json={"type": "slide_deck", "notebook_id": nb["id"], | |
| "document_ids": [pricing["id"]], "deck_length": "long", "instructions": "For new writers"}, | |
| headers=auth).json() | |
| run_jobs() | |
| done = client.get(f"/api/artifacts/{art['id']}", headers=auth).json() | |
| assert done["status"] == "ready", done["content"].get("error") | |
| slides = done["content"]["deck"]["slides"] | |
| assert 12 <= len(slides) <= 16 and done["content"]["length"] == "long" | |
| sections = [s["heading"] for s in slides if s["layout"] == "section"] | |
| assert slides[1]["layout"] == "agenda" and [p["text"] for p in slides[1]["points"]] == sections # the parts | |
| assert all(w["request"] == "For new writers" for w in log["WriteSlide"]) and seen["closing"] == "For new writers" | |
| assert llm["_calls"].count("OutlineDeck") == 1 # nothing missed: no second plan | |
| def test_a_missed_ask_is_planned_again(client, auth, run_jobs, feed, llm, monkeypatch, no_chrome): # noqa: F811 | |
| pricing, nb = _notebook_with_pricing(client, auth, run_jobs) | |
| _fake_many(monkeypatch, pricing["path"], {}) | |
| requests: list[str] = [] | |
| first, second = _outline(4), _outline(5) | |
| def outline(**kw): | |
| requests.append(kw["request"]) | |
| out = (first if len(requests) == 1 else second)(**kw) | |
| if len(requests) > 1: | |
| out["outline"]["slides"][-1]["heading"] = "Pricing for beginners" | |
| return out | |
| llm["OutlineDeck"] = outline | |
| llm["CheckRequest"] = {"asks": [{"ask": "a slide for beginners", "covered": False}, {"ask": "pricing", "covered": True}]} | |
| llm["WriteClosing"] = CLOSING | |
| art = client.post("/api/artifacts/generate", json={"type": "slide_deck", "notebook_id": nb["id"], | |
| "document_ids": [pricing["id"]], "deck_length": "short", | |
| "instructions": "Pricing, with a slide for beginners"}, headers=auth).json() | |
| run_jobs() | |
| slides = client.get(f"/api/artifacts/{art['id']}", headers=auth).json()["content"]["deck"]["slides"] | |
| assert len(requests) == 2 and "a slide for beginners" in requests[1] and "pricing" not in requests[1].split("supports: ")[1] | |
| assert any(s["heading"] == "Pricing for beginners" for s in slides) | |
| assert client.post("/api/artifacts/generate", json={"type": "slide_deck", "notebook_id": nb["id"], | |
| "document_ids": [pricing["id"]], "deck_length": "huge"}, headers=auth).status_code == 422 | |
| def test_the_closing_is_tried_again_and_a_repeated_heading_goes(client, auth, run_jobs, feed, llm, monkeypatch, no_chrome): # noqa: F811 | |
| pricing, nb = _notebook_with_pricing(client, auth, run_jobs) | |
| _fake_many(monkeypatch, pricing["path"], {}) | |
| outline = _outline(6, ("points", "section", "points", "quote", "points", "two_column")) | |
| def with_repeat(**kw): | |
| out = outline(**kw) | |
| out["outline"]["slides"][1]["heading"] = out["outline"]["slides"][2]["heading"] # a part titled like its slide | |
| return out | |
| llm["OutlineDeck"] = with_repeat | |
| tries = [] | |
| def closing(**kw): | |
| tries.append(1) | |
| if len(tries) == 1: | |
| raise RuntimeError("model hiccup") | |
| return CLOSING | |
| llm["WriteClosing"] = closing | |
| art = client.post("/api/artifacts/generate", json={"type": "slide_deck", "notebook_id": nb["id"], | |
| "document_ids": [pricing["id"]], "deck_length": "short"}, headers=auth).json() | |
| run_jobs() | |
| slides = client.get(f"/api/artifacts/{art['id']}", headers=auth).json()["content"]["deck"]["slides"] | |
| assert len(tries) == 2 and slides[-1]["closing"] == "Price the work like it matters." | |
| headings = [s["heading"] for s in slides] | |
| assert len(headings) == len(set(headings)) and 6 <= len(slides) <= 8 | |
| def test_light_planets_sit_on_their_ring(key): | |
| """A planet beside rings is drawn on one of them, tilt included, whatever the seed.""" | |
| import math | |
| from app.slides.decor import build | |
| for seed in range(40): | |
| shapes = build("light-space", *key, seed, 0).shapes | |
| rings = [s for s in shapes if s.kind == "ring"] | |
| for p in (s for s in shapes if s.kind == "planet"): | |
| def off(r, p=p): # how far the planet's centre is from the ring's line, in units of the ring | |
| rot = math.radians(-r.rot) | |
| dx, dy = p.cx - r.cx, p.cy - r.cy | |
| x, y = dx * math.cos(rot) - dy * math.sin(rot), dx * math.sin(rot) + dy * math.cos(rot) | |
| return abs(math.hypot(x / r.rx, y / r.ry) - 1) | |
| assert min(off(r) for r in rings) < 1e-6, (key, seed) | |
| def test_dark_rings_wrap_round_their_planet(key): | |
| """A ringed planet's far half is drawn before the planet (hidden behind it) and the near half after (in front).""" | |
| from app.slides.decor import FAR, NEAR, build | |
| for seed in range(10): | |
| shapes = build("dark-space", *key, seed, 0).shapes | |
| at = next(i for i, s in enumerate(shapes) if s.kind == "planet") | |
| rings = [(i, s) for i, s in enumerate(shapes) if s.kind in ("ring", "band")] | |
| assert rings and all(s.arc for _, s in rings) | |
| assert all(i < at for i, s in rings if s.arc == FAR) and all(i > at for i, s in rings if s.arc == NEAR) | |
| assert all(s.rx * s.inner > shapes[at].r for _, s in rings if s.kind == "band") # the band clears the planet | |
| def test_every_variant_marks_its_editable_text(theme): | |
| """Every piece of a slide's own text is drawn in a span the editor can find, with the field's real limit; numbers | |
| and footers are not editable.""" | |
| from app.slides.content import field_limit | |
| for fmt in ("detailed", "presenter"): | |
| for layout, variants in GEOMETRY[theme].items(): | |
| for v in variants: | |
| deck = clamp_deck({"title": "T", "slides": [{"layout": layout, **FULL[layout]}]}, fmt) | |
| deck["slides"][0]["variant"] = v | |
| html = slide_html(deck, 0, theme, fmt, 3) | |
| found = dict(re.findall(r'data-f="([^"]+)" data-max="(\d+)"', html)) | |
| assert "heading" in found or layout in ("quote", "closing"), (layout, v) | |
| assert all(int(n) == field_limit(layout, f, fmt) for f, n in found.items()) | |
| s = deck["slides"][0] | |
| want = {"points": [f"points.{i}.text" for i in range(len(s["points"]))], "stat": ["stat.value", "stat.label"], | |
| "quote": ["quote.text", "quote.by"], "two_column": ["left.label", "right.items.1"], | |
| "closing": [f"takeaways.{i}" for i in range(len(s["takeaways"]))] + ["closing"]}.get(layout, []) | |
| assert set(want) <= set(found), (fmt, layout, v, found) | |
| assert not any(f.startswith("footer") for f in found) | |
| def _ready_deck(client, auth, run_jobs, monkeypatch, llm): # noqa: F811 | |
| pricing, nb = _notebook_with_pricing(client, auth, run_jobs) | |
| _fake_many(monkeypatch, pricing["path"], {}) | |
| llm["OutlineDeck"] = _outline(5) | |
| llm["WriteClosing"] = CLOSING | |
| art = client.post("/api/artifacts/generate", json={"type": "slide_deck", "notebook_id": nb["id"], | |
| "document_ids": [pricing["id"]], "deck_length": "short"}, headers=auth).json() | |
| run_jobs() | |
| return client.get(f"/api/artifacts/{art['id']}", headers=auth).json() | |
| def test_edited_deck_is_saved_fitted_and_downloaded(client, auth, run_jobs, feed, llm, monkeypatch, no_chrome): # noqa: F811 | |
| done = _ready_deck(client, auth, run_jobs, monkeypatch, llm) | |
| slides = copy.deepcopy(done["content"]["deck"]["slides"]) | |
| assert len(done["content"]["slide_slots"]) == len(slides) and "heading" in done["content"]["slide_slots"][0] | |
| pi = next(i for i, s in enumerate(slides) if s["points"] and s["layout"] == "points") | |
| slides[0]["heading"] = "A brand new title" | |
| slides[0]["notes"] = "Say hello first." | |
| slides[pi]["points"][0]["text"] = "Edited point " + "very long " * 60 | |
| slides[-1]["takeaways"][1] = " An <b>edited</b> takeaway " | |
| url = f"/api/artifacts/{done['id']}/deck" | |
| saved = client.put(url, json={"slides": slides}, headers=auth) | |
| assert saved.status_code == 200, saved.text | |
| after = saved.json()["content"] | |
| s = after["deck"]["slides"] | |
| assert s[0]["heading"] == "A brand new title" and s[0]["notes"] == "Say hello first." | |
| assert s[pi]["points"][0]["text"].startswith("Edited point") and len(s[pi]["points"][0]["text"]) <= LIMITS["detailed"]["text"] | |
| assert s[-1]["takeaways"][1] == "An edited takeaway" # plain text only | |
| before = done["content"]["deck"]["slides"] | |
| assert [(x["layout"], x["variant"]) for x in s] == [(x["layout"], x["variant"]) for x in before] # designs kept | |
| assert "A brand new title" in after["slides_html"][0] | |
| pptx = client.get(f"/api/artifacts/{done['id']}/slides.pptx", headers=auth) | |
| texts = [sh.text_frame.text for sh in Presentation(io.BytesIO(pptx.content)).slides[0].shapes if sh.has_text_frame] | |
| assert any("A brand new title" in t for t in texts) | |
| assert client.put(url, json={"slides": []}, headers=auth).status_code == 422 | |
| assert client.put(url, json={"slides": [{"layout": "points", "heading": ""}]}, headers=auth).status_code == 400 | |
| assert client.put(url, json={"slides": [slides[0]] * 31}, headers=auth).status_code == 400 | |
| kept = client.get(f"/api/artifacts/{done['id']}", headers=auth).json()["content"]["deck"]["slides"] | |
| assert kept[0]["heading"] == "A brand new title" and len(kept) == len(slides) # a refused save changes nothing | |
| def test_slides_can_be_added_removed_moved_and_change_layout(client, auth, run_jobs, feed, llm, monkeypatch, no_chrome): # noqa: F811 | |
| done = _ready_deck(client, auth, run_jobs, monkeypatch, llm) | |
| slides = copy.deepcopy(done["content"]["deck"]["slides"]) | |
| n = len(slides) | |
| pi = next(i for i, s in enumerate(slides) if s["layout"] == "points") | |
| edited = copy.deepcopy(slides) | |
| edited[pi]["points"].append({"term": "", "text": "A point added by hand"}) # a text box added | |
| edited[pi]["lead"] = "" # and one removed | |
| stat = {"layout": "stat", "variant": "", "heading": "A new stat slide", "stat": {"value": "42%", "label": "Readers who stayed"}} | |
| edited.insert(1, stat) # a slide added | |
| edited.insert(2, copy.deepcopy(edited[0]) | {"variant": ""}) # a slide duplicated | |
| del edited[-2] # a slide removed | |
| edited[-1], edited[-2] = edited[-2], edited[-1] # two moved | |
| url = f"/api/artifacts/{done['id']}" | |
| prev = client.post(f"{url}/deck-preview", json={"slides": edited}, headers=auth) | |
| assert prev.status_code == 200, prev.text | |
| got = prev.json() | |
| out = got["slides"] | |
| assert len(out) == n + 1 and len(got["slides_html"]) == n + 1 and len(got["slide_slots"]) == n + 1 | |
| assert out[1]["layout"] == "stat" and out[1]["variant"] and "42%" in got["slides_html"][1] | |
| assert "value" in got["slide_slots"][1] and "label" in got["slide_slots"][1] | |
| moved = next(s for s in out if s["heading"] == slides[pi]["heading"]) | |
| assert moved["points"][-1]["text"] == "A point added by hand" and moved["lead"] == "" | |
| pairs = [(x["layout"], x["variant"]) for x in out] | |
| assert all(a != b for a, b in zip(pairs, pairs[1:], strict=False)) # no two alike in a row | |
| assert client.get(url, headers=auth).json()["content"]["deck"]["slides"] == done["content"]["deck"]["slides"] # preview saves nothing | |
| # a layout change: the points slide becomes a quote slide | |
| q = next(i for i, s in enumerate(edited) if s["layout"] == "points") | |
| edited[q] = {**edited[q], "layout": "quote", "variant": "", "quote": {"text": "Price the work like it matters.", "by": "A writer"}} | |
| saved = client.put(f"{url}/deck", json={"slides": edited}, headers=auth).json()["content"] | |
| assert saved["deck"]["slides"][q]["layout"] == "quote" and saved["deck"]["slides"][q]["variant"] | |
| assert len(saved["deck"]["slides"]) == n + 1 | |
| pptx = client.get(f"{url}/slides.pptx", headers=auth) | |
| assert len(Presentation(io.BytesIO(pptx.content)).slides) == n + 1 | |
| def test_fill_variants_keeps_good_variants_and_fixes_the_rest(): | |
| from app.slides.build import fill_variants | |
| deck = clamp_deck({"title": "T", "slides": [{"layout": lay, **FULL[lay]} for lay in ("title", "points", "points", "stat", "closing")]}, | |
| "detailed") | |
| assign_variants(deck["slides"], "dark-space", seed=3) | |
| before = [s["variant"] for s in deck["slides"]] | |
| fill_variants(deck["slides"], "dark-space", 3) | |
| assert [s["variant"] for s in deck["slides"]] == before # nothing to fix: nothing changes | |
| deck["slides"][3]["variant"] = "" # new | |
| deck["slides"][2]["variant"] = deck["slides"][1]["variant"] # the same as the slide before it | |
| fill_variants(deck["slides"], "dark-space", 3) | |
| assert deck["slides"][3]["variant"] in VARIANTS["stat"] | |
| assert deck["slides"][2]["variant"] != deck["slides"][1]["variant"] | |
| assert [s["variant"] for s in deck["slides"]][:2] == before[:2] and deck["slides"][4]["variant"] == before[4] | |