diff --git a/PUBLIC_SOURCE_MANIFEST.json b/PUBLIC_SOURCE_MANIFEST.json index 1e52265..6fe1de4 100644 --- a/PUBLIC_SOURCE_MANIFEST.json +++ b/PUBLIC_SOURCE_MANIFEST.json @@ -82,8 +82,8 @@ }, { "path": "README.md", - "sha256": "a1871e26e9c458960a35aec358baf2d818e3a8a4a12c2ecba247b0966ed207ae", - "size_bytes": 10242 + "sha256": "f9646d68ef1c9896ee66ae2752a9f8eedcb1319141d98bb7e9f1864022a4c9be", + "size_bytes": 10368 }, { "path": "SECURITY.md", @@ -100,6 +100,16 @@ "sha256": "4dd2b88529c6238c9a38076e8aa28ffccaa1cd0442ad3c5b06923ee8dd4371c0", "size_bytes": 153512 }, + { + "path": "assets/attentrix/attentrix-readme-lockup-dark.png", + "sha256": "d185f4e9e572cccb1b29fbbbadc54e0d084e4c31bc06df7dac3138525ab06896", + "size_bytes": 46851 + }, + { + "path": "assets/attentrix/attentrix-readme-lockup-light.png", + "sha256": "5af0ce26e3a02701e4a21d5068edc92f09e8dc4381e06984a562917deb25314b", + "size_bytes": 64898 + }, { "path": "configs/README.md", "sha256": "573f5a668ad4e419ece3452f1bb7dfcda2bf55463871f71b5328e6daca2a556d", @@ -1492,8 +1502,8 @@ }, { "path": "public_repository.toml", - "sha256": "383ed5c30aa539434491a2f100f90ea096f8b011fabb202e2948e8786029094b", - "size_bytes": 3069 + "sha256": "4f7e613b6e320faafcfe122eebb237086a147ae7720d631be092d0d8e1f45e0c", + "size_bytes": 3180 }, { "path": "pyproject.toml", @@ -1822,8 +1832,8 @@ }, { "path": "scripts/generate_attentrix_brand_assets.py", - "sha256": "3ff01899564c8573943ccbb42cabe703650f0cd2e05a52d1afe95d74eabeb292", - "size_bytes": 1312 + "sha256": "a80a0b43613d084c3144889a8cda1451fa0befdea70ca91eb83ae13ada0b52f1", + "size_bytes": 2207 }, { "path": "scripts/generate_research_slideshow.py", @@ -2632,8 +2642,8 @@ }, { "path": "src/tavc/signalroom/branding.py", - "sha256": "3593d2b2a2b7c6f605e36a9b371aea8c52220326232f9e5b93f3a98bee90ca67", - "size_bytes": 2377 + "sha256": "af2eea017bd7b612e135f2d59ed3f7cf25ada535954a69a5e05b7c4c074ee40b", + "size_bytes": 4021 }, { "path": "src/tavc/signalroom/confusion_specialist.py", @@ -2887,8 +2897,8 @@ }, { "path": "tests/test_attentrix_brand_assets.py", - "sha256": "6cf7251e28c60ad0acd53b371ad0b3a6ae859117d206103565555852974d6fde", - "size_bytes": 4461 + "sha256": "6575360070bc5422b5b28c38a03e6049b2ac39d2142c0469789828f5fe569f00", + "size_bytes": 6048 }, { "path": "tests/test_attentrix_pre_paper_evidence.py", @@ -3540,6 +3550,6 @@ "product": "ATTENTRIX", "product_version": "0.2.0-beta.4", "schema_version": "attentrix_public_source_v1", - "source_revision": "1ef9dbb7e4ad46d0268b96d315bc6ee9fcb8f060", + "source_revision": "131fdd24bda90882fc45028bcace4489b552b1e7", "source_worktree_dirty": false } diff --git a/README.md b/README.md index 6a0f2f0..3fb2ddd 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@

- ATTENTRIX logo + ATTENTRIX + ATTENTRIX

-# ATTENTRIX - ATTENTRIX is a local research application that turns visible audience cues in a Zoom gallery into glanceable presenter feedback. It tracks each measurable participant separately, estimates four visual states, and summarizes the room as a small set of actions: continue, clarify, or re-engage. The current public beta is `0.2.0-beta.4`. Its frozen inference checkpoint is Live v20 (`classroom_v6_signalroom_live_v20`) with the participant-relative presenter policy `gallery_person_relative_v1` and compute profile `primary_hand_1440_q82`. diff --git a/assets/attentrix/attentrix-readme-lockup-dark.png b/assets/attentrix/attentrix-readme-lockup-dark.png new file mode 100644 index 0000000..7fdbf1e Binary files /dev/null and b/assets/attentrix/attentrix-readme-lockup-dark.png differ diff --git a/assets/attentrix/attentrix-readme-lockup-light.png b/assets/attentrix/attentrix-readme-lockup-light.png new file mode 100644 index 0000000..77be4d1 Binary files /dev/null and b/assets/attentrix/attentrix-readme-lockup-light.png differ diff --git a/public_repository.toml b/public_repository.toml index 9d87238..0c9811f 100644 --- a/public_repository.toml +++ b/public_repository.toml @@ -83,6 +83,8 @@ exclude_patterns = [ allowed_binary_patterns = [ "assets/attentrix/attentrix-logo.png", + "assets/attentrix/attentrix-readme-lockup-dark.png", + "assets/attentrix/attentrix-readme-lockup-light.png", "docs/assets/attentrix_compression_evidence_20260716.png", "docs/assets/attentrix_paper_teaser.jpg", "docs/audio_compression_seed_mean.png", diff --git a/scripts/generate_attentrix_brand_assets.py b/scripts/generate_attentrix_brand_assets.py index 9945cae..c982ecd 100644 --- a/scripts/generate_attentrix_brand_assets.py +++ b/scripts/generate_attentrix_brand_assets.py @@ -4,11 +4,17 @@ from pathlib import Path -from tavc.signalroom.branding import save_attentrix_png +from tavc.signalroom.branding import ( + render_attentrix_readme_lockup, + save_attentrix_png, +) ROOT = Path(__file__).resolve().parents[1] CANONICAL_LOGO_PATH = ROOT / "assets" / "attentrix" / "attentrix-logo.png" +CANONICAL_WORDMARK_PATH = ( + ROOT / "website" / "public" / "images" / "attentrix-wordmark.png" +) PNG_TARGETS = ( (ROOT / "website" / "public" / "images" / "attentrix-logo.png", 512), (ROOT / "website" / "public" / "favicon.png", 64), @@ -31,6 +37,16 @@ ), (ROOT / "src" / "tavc" / "app" / "static" / "favicon.png", 64), ) +README_LOCKUP_TARGETS = ( + ( + ROOT / "assets" / "attentrix" / "attentrix-readme-lockup-dark.png", + (255, 255, 255), + ), + ( + ROOT / "assets" / "attentrix" / "attentrix-readme-lockup-light.png", + (14, 29, 49), + ), +) def main() -> int: @@ -38,9 +54,20 @@ def main() -> int: raise FileNotFoundError( f"Canonical ATTENTRIX logo is missing: {CANONICAL_LOGO_PATH}" ) + if not CANONICAL_WORDMARK_PATH.is_file(): + raise FileNotFoundError( + f"Canonical ATTENTRIX wordmark is missing: {CANONICAL_WORDMARK_PATH}" + ) for destination, size in PNG_TARGETS: save_attentrix_png(CANONICAL_LOGO_PATH, destination, size=size) print(f"Generated {destination.relative_to(ROOT)} ({size}x{size})") + for destination, color in README_LOCKUP_TARGETS: + render_attentrix_readme_lockup( + CANONICAL_LOGO_PATH, + CANONICAL_WORDMARK_PATH, + wordmark_color=color, + ).save(destination, format="PNG", optimize=True) + print(f"Generated {destination.relative_to(ROOT)} (1100x220)") return 0 diff --git a/src/tavc/signalroom/branding.py b/src/tavc/signalroom/branding.py index 29c12d0..a77f617 100644 --- a/src/tavc/signalroom/branding.py +++ b/src/tavc/signalroom/branding.py @@ -9,6 +9,7 @@ DEFAULT_PADDING_FRACTION = 1 / 16 RUNTIME_LOGO_PATH = Path(__file__).resolve().parent / "static" / "attentrix-logo.png" +README_LOCKUP_SIZE = (1100, 220) def render_attentrix_logo( @@ -39,6 +40,56 @@ def render_attentrix_logo( return canvas +def render_attentrix_readme_lockup( + logo_source: str | Path, + wordmark_source: str | Path, + *, + wordmark_color: tuple[int, int, int], +) -> Image.Image: + """Render the horizontal emblem and custom wordmark used by GitHub.""" + + if len(wordmark_color) != 3 or any( + not 0 <= channel <= 255 for channel in wordmark_color + ): + raise ValueError("Wordmark color must contain three 8-bit channels.") + + with Image.open(wordmark_source) as opened: + wordmark = opened.convert("RGBA") + visible_alpha = wordmark.getchannel("A").point( + lambda alpha: 255 if alpha >= 16 else 0 + ) + bounds = visible_alpha.getbbox() + if bounds is None: + raise ValueError("The ATTENTRIX wordmark contains no visible pixels.") + + margin = 4 + left, top, right, bottom = bounds + crop_bounds = ( + max(0, left - margin), + max(0, top - margin), + min(wordmark.width, right + margin), + min(wordmark.height, bottom + margin), + ) + alpha = wordmark.getchannel("A").crop(crop_bounds) + recolored = Image.new("RGBA", alpha.size, (*wordmark_color, 0)) + recolored.putalpha(alpha) + recolored.thumbnail((840, 98), Image.Resampling.LANCZOS) + + canvas = Image.new("RGBA", README_LOCKUP_SIZE, (0, 0, 0, 0)) + emblem = render_attentrix_logo( + logo_source, + size=180, + padding_fraction=1 / 24, + ) + canvas.alpha_composite(emblem, (20, 20)) + wordmark_offset = ( + 230, + (README_LOCKUP_SIZE[1] - recolored.height) // 2, + ) + canvas.alpha_composite(recolored, wordmark_offset) + return canvas + + def save_attentrix_png( source: str | Path, destination: str | Path, diff --git a/tests/test_attentrix_brand_assets.py b/tests/test_attentrix_brand_assets.py index 873f4e9..21eef33 100644 --- a/tests/test_attentrix_brand_assets.py +++ b/tests/test_attentrix_brand_assets.py @@ -7,6 +7,7 @@ from PIL import Image from tavc.signalroom.branding import ( + render_attentrix_readme_lockup, render_attentrix_logo, save_attentrix_ico, ) @@ -14,6 +15,9 @@ ROOT = Path(__file__).resolve().parents[1] CANONICAL_LOGO = ROOT / "assets" / "attentrix" / "attentrix-logo.png" +CANONICAL_WORDMARK = ( + ROOT / "website" / "public" / "images" / "attentrix-wordmark.png" +) class AttentrixBrandAssetTests(unittest.TestCase): @@ -70,6 +74,45 @@ def test_windows_icon_contains_standard_sizes(self): sizes = icon.info.get("sizes", set()) self.assertTrue({(16, 16), (32, 32), (48, 48), (256, 256)}.issubset(sizes)) + def test_readme_lockup_keeps_the_logo_and_recolors_the_exact_wordmark(self): + white_lockup = render_attentrix_readme_lockup( + CANONICAL_LOGO, + CANONICAL_WORDMARK, + wordmark_color=(255, 255, 255), + ) + navy_lockup = render_attentrix_readme_lockup( + CANONICAL_LOGO, + CANONICAL_WORDMARK, + wordmark_color=(14, 29, 49), + ) + + self.assertEqual(white_lockup.mode, "RGBA") + self.assertEqual(white_lockup.size, (1100, 220)) + self.assertIsNotNone(white_lockup.getbbox()) + self.assertGreater( + sum( + 1 + for red, green, blue, alpha in white_lockup.get_flattened_data() + if alpha > 200 and (red, green, blue) == (255, 255, 255) + ), + 1_000, + ) + self.assertGreater( + sum( + 1 + for red, green, blue, alpha in navy_lockup.get_flattened_data() + if alpha > 200 and (red, green, blue) == (14, 29, 49) + ), + 1_000, + ) + + def test_readme_uses_theme_aware_horizontal_brand_lockups(self): + readme = (ROOT / "README.md").read_text(encoding="utf-8") + + self.assertIn("attentrix-readme-lockup-dark.png#gh-dark-mode-only", readme) + self.assertIn("attentrix-readme-lockup-light.png#gh-light-mode-only", readme) + self.assertNotIn("\n# ATTENTRIX\n", readme) + def test_product_surfaces_reference_the_canonical_generated_assets(self): website_layout = (ROOT / "website" / "app" / "layout.tsx").read_text( encoding="utf-8"