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
-
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"