diff --git a/README.md b/README.md index 53167a7..d1bec9f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- ARTEMIS Banner + ARTEMIS Banner

diff --git a/README_CN.md b/README_CN.md index 4a2cf02..9da5721 100644 --- a/README_CN.md +++ b/README_CN.md @@ -1,5 +1,5 @@

- ARTEMIS Banner + ARTEMIS Banner

diff --git a/docs/assets/artemis-banner-cn.png b/docs/assets/artemis-banner-cn.png index b8b1748..f0f7022 100644 Binary files a/docs/assets/artemis-banner-cn.png and b/docs/assets/artemis-banner-cn.png differ diff --git a/docs/assets/artemis-banner-en.png b/docs/assets/artemis-banner-en.png index f003fd8..f0f7022 100644 Binary files a/docs/assets/artemis-banner-en.png and b/docs/assets/artemis-banner-en.png differ diff --git a/docs/assets/artemis-banner-raw.jpg b/docs/assets/artemis-banner-raw.jpg new file mode 100644 index 0000000..3cc0e1a Binary files /dev/null and b/docs/assets/artemis-banner-raw.jpg differ diff --git a/docs/assets/artemis-banner.png b/docs/assets/artemis-banner.png index f003fd8..f0f7022 100644 Binary files a/docs/assets/artemis-banner.png and b/docs/assets/artemis-banner.png differ diff --git a/docs/assets/generate_banner.py b/docs/assets/generate_banner.py index e2c4232..670e555 100644 --- a/docs/assets/generate_banner.py +++ b/docs/assets/generate_banner.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 # Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,723 +13,187 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os -import subprocess -from pathlib import Path - - -def create_banner_html(lang="en"): - html_template = """ - - - - - - - - -

-
-
-
-
- - -
-

ARTEMIS

- -
- __TAGLINE__ -
- - -
-
- __PROMPT_LABEL__ -
-
- __PROMPT_STATUS__ -
-
-
- > __PROMPT_TEXT__ -
-
-
- - -
- - -
-
-
-
- __CONSOLE_TITLE__ -
- __CONSOLE_BADGE__ -
- -
-
- ✓ - __STEP_1__ -
-
- ✓ - __STEP_2__ -
- - -
-
- __STEP_3_TITLE__ - __STEP_3_TAG__ -
-
- CPU < 5.0% - 4.2% PASS -
-
-
-
- - -
-
-
- -
- __SCREEN_TITLE__ - ● PIXEL -
- -
-
🎵
-
- Coldplay - Yellow - Pixel Buds Pro -
- ▶ -
- -
- -
-
🎯 GROUNDED
-
-
- __MOCK_BTN__ -
- -
-
-
-
-
-
-
- -
- - - +"""ARTEMIS Official Banner Generator. + +- Background: High-resolution celestial space background cropped to 1376x380 and + seamlessly extended on the right side to 1800x380 (+424px pure dark space) + to balance visual weight and provide ample breathing room for the crescent moon. +- Typography: Montserrat Light (weight 300) with wide tracking (64px) at 98px size. +- Centering: Exact pixel-level ink bounding box alignment, ensuring true mathematical + and optical center at exactly (width/2, height/2). +- Sharpness: 3x vector supersampling followed by Lanczos downsampling for razor-sharp + edges without rasterization stair-stepping or blurry halation. +- Lighting: Soft celestial ambient aura layered behind the pure white text. +- Portability: Automatically downloads and caches the Montserrat font in /tmp + without requiring font binaries committed to the repository. """ - if lang == "cn": - replacements = { - "__TAGLINE__": "让 AI 助手与测试套件像人一样直接操作真机", - "__PROMPT_LABEL__": "🎯 测试指令", - "__PROMPT_STATUS__": "真机运行中", - "__PROMPT_TEXT__": "连接蓝牙耳机播放音乐,断言 CPU 占用 < 5%", - "__CONSOLE_TITLE__": "真机测试执行引擎", - "__CONSOLE_BADGE__": "通过", - "__STEP_1__": "连接蓝牙耳机", - "__STEP_2__": "打开播放器并播放歌曲", - "__STEP_3_TITLE__": "▶ 断言性能指标", - "__STEP_3_TAG__": "实时验证", - "__SCREEN_TITLE__": "正在播放", - "__MOCK_BTN__": "音频输出控制", - } + +import io +import os +import shutil +import urllib.request +import numpy as np +from PIL import Image, ImageDraw, ImageFont, ImageFilter + + +def get_font_path(weight: int = 300) -> str: + """Retrieve or download Montserrat font instantiated at the requested weight.""" + cache_dir = "/tmp/artemis_banner_fonts" + os.makedirs(cache_dir, exist_ok=True) + target_font = os.path.join(cache_dir, f"Montserrat-wght{weight}.ttf") + + if os.path.exists(target_font): + return target_font + + var_url = "https://raw.githubusercontent.com/google/fonts/main/ofl/montserrat/Montserrat%5Bwght%5D.ttf" + print(f"Fetching Montserrat font from Google Fonts ({var_url}) ...") + try: + req = urllib.request.Request(var_url, headers={"User-Agent": "Mozilla/5.0"}) + with urllib.request.urlopen(req, timeout=15) as resp: + font_data = resp.read() + + from fontTools.ttLib import TTFont + from fontTools.varLib.instancer import instantiateVariableFont + + tt = TTFont(io.BytesIO(font_data)) + inst = instantiateVariableFont(tt, {"wght": weight}) + inst.save(target_font) + print(f"Successfully cached Montserrat (wght {weight}) to: {target_font}") + return target_font + except Exception as e: + print(f"Warning: Failed to fetch online font: {e}. Falling back to default system font.") + return None + + +def extend_canvas(canvas: Image.Image, extra_w: int = 424, blend_w: int = 80) -> Image.Image: + """Seamlessly extends the right edge of the canvas with matching space background.""" + if extra_w <= 0: + return canvas + w, h = canvas.size + canvas_rgb = canvas.convert("RGB") + right_col = np.array(canvas_rgb)[:, -1:, :] + ext_part = np.repeat(right_col, extra_w + blend_w, axis=1) + canvas_arr = np.array(canvas_rgb) + + alpha = np.linspace(0, 1, blend_w).reshape(1, blend_w, 1) + blend_zone = canvas_arr[:, w - blend_w : w, :] * (1 - alpha) + ext_part[:, :blend_w, :] * alpha + canvas_arr[:, w - blend_w : w, :] = blend_zone.astype(np.uint8) + + full_arr = np.concatenate([canvas_arr, ext_part[:, blend_w:, :]], axis=1) + return Image.fromarray(full_arr.astype(np.uint8)).convert("RGBA") + + +def generate_banner( + bg_path: str, + output_path: str, + text: str = "ARTEMIS", + font_size: int = 98, + tracking: int = 64, + font_weight: int = 300, + extra_w: int = 424, + offset_x: int = 160, + glow_mode: str = "ambient", + supersample: int = 3, +): + """Generate the official ARTEMIS banner with high clarity and optical/geometric centering.""" + if not os.path.exists(bg_path): + raise FileNotFoundError(f"Background image not found: {bg_path}") + + # 1. Load raw background and crop canvas + raw_img = Image.open(bg_path) + w, h = raw_img.size + canvas = raw_img.crop((0, 15, w, 395)).convert("RGBA") + + # 2. Seamlessly extend the right side + if extra_w > 0: + canvas = extend_canvas(canvas, extra_w=extra_w) + cw, ch = canvas.size + + # 3. Supersampled text layout + scale = max(1, supersample) + sw, sh = cw * scale, ch * scale + sfsize = font_size * scale + strk = tracking * scale + + font_path = get_font_path(weight=font_weight) + if font_path and os.path.exists(font_path): + font = ImageFont.truetype(font_path, sfsize) else: - replacements = { - "__TAGLINE__": "Let AI assistants and test suites use real phones like a human", - "__PROMPT_LABEL__": "🎯 TEST PROMPT", - "__PROMPT_STATUS__": "RUNNING", - "__PROMPT_TEXT__": "Connect Bluetooth earbuds, play song, and assert CPU < 5%", - "__CONSOLE_TITLE__": "Test Runner", - "__CONSOLE_BADGE__": "PASSED", - "__STEP_1__": "Connect Bluetooth earbuds", - "__STEP_2__": "Launch player & play song", - "__STEP_3_TITLE__": "▶ Assert Performance SLA", - "__STEP_3_TAG__": "VERIFYING", - "__SCREEN_TITLE__": "Now Playing", - "__MOCK_BTN__": "Audio Output", - } + font = ImageFont.load_default() - for key, val in replacements.items(): - html_template = html_template.replace(key, val) - return html_template + t_layer = Image.new("RGBA", (sw * 2, sh * 2), (0, 0, 0, 0)) + draw = ImageDraw.Draw(t_layer) + start_x = 400 * scale + start_y = 400 * scale + curr_x = start_x + for c in text: + draw.text((curr_x, start_y), c, font=font, fill=(255, 255, 255, 255)) + curr_x += font.getlength(c) + strk + # 4. Extract ink bounds and downsample with Lanczos + ink_3x = t_layer.crop(t_layer.getbbox()) + target_w = round(ink_3x.width / scale) + target_h = round(ink_3x.height / scale) + ink_1x = ink_3x.resize((target_w, target_h), Image.Resampling.LANCZOS) + iw, ih = ink_1x.size -def render_image(html_str, output_path): - temp_html = f"/tmp/banner_{os.path.basename(output_path)}.html" - with open(temp_html, "w", encoding="utf-8") as f: - f.write(html_str) + # 5. Exact pixel geometric / optical centering + px = (cw - iw) // 2 + offset_x + py = (ch - ih) // 2 - cmd = [ - "google-chrome", - "--headless", - "--disable-gpu", - "--no-sandbox", - "--disable-dev-shm-usage", - "--window-size=1280,480", - "--hide-scrollbars", - f"--screenshot={output_path}", - f"file://{temp_html}", - ] - res = subprocess.run(cmd, capture_output=True, text=True) - if os.path.exists(output_path): - print(f"Rendered {output_path} ({os.path.getsize(output_path)} bytes)") - else: - print(f"Error rendering {output_path}: {res.stderr}") + # 6. Lighting and composite + composite_img = canvas.copy() + + if glow_mode == "ambient": + g = Image.new("RGBA", (cw, ch), (0, 0, 0, 0)) + cyan = Image.new("RGBA", (iw, ih), (120, 215, 255, 55)) + g_ink = Image.composite(cyan, Image.new("RGBA", (iw, ih), (0, 0, 0, 0)), ink_1x.split()[3]) + g.paste(g_ink, (px, py), g_ink) + g = g.filter(ImageFilter.GaussianBlur(18)) + composite_img = Image.alpha_composite(composite_img, g) + + # Top crisp white text + text_overlay = Image.new("RGBA", (cw, ch), (0, 0, 0, 0)) + text_overlay.paste(ink_1x, (px, py), ink_1x) + final_img = Image.alpha_composite(composite_img, text_overlay).convert("RGB") + + os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) + final_img.save(output_path, "PNG") + + center_x = px + iw / 2.0 + center_y = py + ih / 2.0 + print(f"Banner successfully generated: {output_path} ({cw}x{ch})") + print(f"Ink dimensions: {iw}x{ih}px, Exact center: ({center_x:.1f}, {center_y:.1f}) [Canvas center: ({cw/2:.1f}, {ch/2:.1f}), Offset X: {offset_x:+d}px]") if __name__ == "__main__": - assets_dir = Path(__file__).resolve().parent - os.makedirs(assets_dir, exist_ok=True) + import argparse - # Generate English main banner and en banner - en_html = create_banner_html(lang="en") - render_image(en_html, str(assets_dir / "artemis-banner.png")) - render_image(en_html, str(assets_dir / "artemis-banner-en.png")) + parser = argparse.ArgumentParser(description="ARTEMIS Official Banner Generator") + parser.add_argument("--offset-x", type=int, default=160, help="Horizontal pixel offset to the right from mathematical center (default: 160)") + parser.add_argument("--tracking", type=int, default=64, help="Character tracking spacing (default: 64)") + parser.add_argument("--font-size", type=int, default=98, help="Font size in pixels (default: 98)") + args = parser.parse_args() - # Generate Chinese banner - cn_html = create_banner_html(lang="cn") - render_image(cn_html, str(assets_dir / "artemis-banner-cn.png")) + assets_dir = os.path.dirname(os.path.abspath(__file__)) + bg_file = os.path.join(assets_dir, "artemis-banner-raw.jpg") + out_file = os.path.join(assets_dir, "artemis-banner.png") + out_file_cn = os.path.join(assets_dir, "artemis-banner-cn.png") + out_file_en = os.path.join(assets_dir, "artemis-banner-en.png") + + generate_banner( + bg_path=bg_file, + output_path=out_file, + text="ARTEMIS", + font_size=args.font_size, + tracking=args.tracking, + font_weight=300, + extra_w=424, + offset_x=args.offset_x, + glow_mode="ambient", + ) + + if os.path.exists(out_file): + shutil.copyfile(out_file, out_file_cn) + shutil.copyfile(out_file, out_file_en) + print(f"Synchronized copy to: {out_file_cn} and {out_file_en}")