Spaces:
Runtime error
Runtime error
File size: 34,669 Bytes
fa8df4c 4a3d0a5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 | """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
@pytest.mark.parametrize("theme", ["dark-space", "light-space"])
@pytest.mark.parametrize("fmt", ["detailed", "presenter"])
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
@pytest.mark.skipif(not chrome(), reason="no Chrome to print with")
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 ------------------------------------------------------------------------------
@pytest.fixture()
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
@pytest.mark.parametrize("key", [("title", "a"), ("title", "c"), ("section", "a"), ("section", "b")])
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)
@pytest.mark.parametrize("key", [("title", "b"), ("points", "a")])
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
@pytest.mark.parametrize("theme", ["dark-space", "light-space"])
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]
|