feat(credits): public CC-BY/BY-SA attribution colophon + CC BY-SA work license

CC BY / CC BY-SA footage needs its author + license shown to the viewer, and the
installation alters every frame (a derivative). Per-clip license/source already
shipped in clips.json but only surfaced in the dev panel and internal review
pages — the public experience showed nothing, so the static benstull.art site
was not attribution-compliant.

Option A — add a manifest-driven credits/colophon (credits.html + credits.js):
the page fetches the same clips the experience uses (baked clips.json in static,
the live API otherwise) and renders one attribution block per clip (title,
license, source), HTML-escaped, sorted, never silently dropping a clip. An
"ⓘ Credits & licenses" link in the header (localised en/es/fr/ja) makes it
reachable from the work — what CC's "reasonable to the medium" calls for. Audio
is all PD/CC0, credited as a courtesy block.

Option D1 — the colophon declares the altered work itself is offered under
CC BY-SA 4.0 and states the footage is modified, satisfying the ShareAlike +
"indicate changes" obligations for the BY-SA sources. Non-commercial.

Ships in the static build (added to PUBLIC_ASSETS). Verified: 41/41 clips
credited incl. all 7 CC-BY/BY-SA clips, 0 missing a license. Tests: node units
for creditEntries/creditsListHtml + escaping + page structure/declaration;
pytest build-output assertion; Playwright e2e (page lists attributions, header
links through).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-06-30 08:39:26 -07:00
parent 3a45e338f3
commit 1c7f2b7785
9 changed files with 318 additions and 3 deletions
+3
View File
@@ -19,6 +19,9 @@ def test_build_emits_frontend_baked_json_and_only_referenced_media(tmp_path):
assert (app_dir / "scrub.js").exists()
assert (app_dir / "alteration.js").exists()
assert (app_dir / "config.js").exists()
# Credits/colophon ships so CC-BY / CC-BY-SA attribution is reachable from the public site.
assert (app_dir / "credits.html").exists()
assert (app_dir / "credits.js").exists()
assert not (app_dir / "author.html").exists()
assert not list(app_dir.glob("review*.html"))
+81
View File
@@ -0,0 +1,81 @@
"""E2E (Playwright) — the public credits/colophon page actually surfaces the
CC-BY / CC-BY-SA video attribution to a visitor.
Unit tests pin the credits *logic* (creditEntries/creditsListHtml) and the build
ships the files; this confirms the live integration: credits.js fetches the clips
and fills the page in a real browser, and the experience links to it. The suite
skips cleanly when Playwright/Chromium is absent (headless box)."""
import socket
import threading
import time
from contextlib import closing
import pytest
pytest.importorskip("playwright.sync_api")
def _free_port() -> int:
with closing(socket.socket()) as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
@pytest.fixture(scope="module")
def app_url():
import uvicorn
from simulator.app import app
port = _free_port()
server = uvicorn.Server(uvicorn.Config(app, host="127.0.0.1", port=port, log_level="error"))
thread = threading.Thread(target=server.run, daemon=True)
thread.start()
for _ in range(50):
if server.started:
break
time.sleep(0.1)
if not server.started:
pytest.skip("uvicorn did not start")
yield f"http://127.0.0.1:{port}"
server.should_exit = True
thread.join(timeout=5)
@pytest.fixture
def browser_page(app_url):
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
try:
browser = p.chromium.launch()
except Exception as exc: # no browser binary installed
pytest.skip(f"chromium not available: {exc}")
page = browser.new_page()
page._app_url = app_url # stash for the tests
yield page
browser.close()
def test_credits_page_lists_video_attributions(browser_page):
page = browser_page
page.goto(page._app_url + "/credits.html")
# credits.js fetches /api/clips and fills the container with one block per clip.
page.wait_for_selector("#video-credits .credit", timeout=30000)
count = page.eval_on_selector_all("#video-credits .credit", "els => els.length")
assert count >= 40, f"expected the full clip pool credited, got {count}"
# the required CC-BY / CC-BY-SA credits are visible to the viewer
body = page.inner_text("body")
assert "NASA, ESA, CSA, STScI" in body
assert "Joe Mabel" in body # a CC BY-SA clip's required author credit
assert "CC BY-SA 4.0" in body # the Option-D1 derivative declaration
def test_experience_links_to_credits(browser_page):
page = browser_page
page.goto(page._app_url + "/")
page.wait_for_selector("#loading", state="detached", timeout=60000)
href = page.get_attribute(".credits-link", "href")
assert href == "credits.html"
page.click(".credits-link")
page.wait_for_selector("#video-credits .credit", timeout=30000)