Files
Brain/brain-h5/scripts/prepare-brain-assets.py
MingNian b0fce840a3 首次提交:脑晴测 H5 项目
包含前端源码、预构建产物(dist/)、零依赖 Node.js 服务器(server.mjs)、
Cloudflare Worker 版本(worker/)、部署文档(DEPLOY.md)和交接文档。

技术总监可直接 clone 后运行 node server.mjs 启动,无需安装 React 或 build。
DEEPSEEK_API_KEY 通过环境变量注入,未纳入仓库。
2026-07-28 10:24:47 +08:00

126 lines
4.5 KiB
Python

from __future__ import annotations
from collections import deque
from pathlib import Path
from PIL import Image
ROOT = Path(__file__).resolve().parents[1]
SOURCE_ROOT = Path("D:/")
OUT_ROOT = ROOT / "public" / "assets" / "brain"
LANCZOS = getattr(getattr(Image, "Resampling", Image), "LANCZOS")
ASSETS = {
"memory/bread.png": "ChatGPT Image 2026年7月23日 21_31_30 (1).png",
"memory/carrot.png": "ChatGPT Image 2026年7月23日 21_31_32 (2).png",
"memory/coffee.png": "ChatGPT Image 2026年7月23日 21_31_34 (4).png",
"memory/cookie.png": "ChatGPT Image 2026年7月23日 21_31_35 (5).png",
"memory/egg.png": "ChatGPT Image 2026年7月23日 21_31_36 (6).png",
"memory/fish.png": "ChatGPT Image 2026年7月23日 21_31_38 (7).png",
"memory/orange.png": "ChatGPT Image 2026年7月23日 21_31_39 (8).png",
"memory/cheese.png": "ChatGPT Image 2026年7月23日 21_31_40.png",
"planes/teal.png": "ChatGPT Image 2026年7月23日 21_31_44 (9).png",
"planes/coral.png": "ChatGPT Image 2026年7月23日 21_31_45 (10).png",
"symbols/camera.png": "ChatGPT Image 2026年7月23日 21_40_52 (1).png",
"symbols/sun.png": "ChatGPT Image 2026年7月23日 21_40_52 (2).png",
"symbols/squares.png": "ChatGPT Image 2026年7月23日 21_40_53 (3).png",
"symbols/lock.png": "ChatGPT Image 2026年7月23日 21_40_53 (4).png",
"symbols/lightning.png": "ChatGPT Image 2026年7月23日 21_40_53 (5).png",
"symbols/moon.png": "ChatGPT Image 2026年7月23日 21_40_53 (6).png",
"symbols/gear.png": "ChatGPT Image 2026年7月23日 21_40_53 (7).png",
"symbols/star.png": "ChatGPT Image 2026年7月23日 21_40_53 (8).png",
"symbols/puzzle.png": "ChatGPT Image 2026年7月23日 21_40_53 (9).png",
"symbols/coffee.png": "ChatGPT Image 2026年7月23日 21_40_53 (10).png",
}
def has_real_alpha(image: Image.Image) -> bool:
if "A" not in image.getbands():
return False
alpha = image.getchannel("A")
lo, hi = alpha.getextrema()
return lo < 255 and hi == 255
def remove_generated_checkerboard(image: Image.Image) -> Image.Image:
rgba = image.convert("RGBA")
rgba.thumbnail((720, 720), LANCZOS)
pixels = rgba.load()
width, height = rgba.size
def is_background(x: int, y: int) -> bool:
red, green, blue, _ = pixels[x, y]
high = max(red, green, blue)
low = min(red, green, blue)
return low >= 218 and high - low <= 18
# Flood only from the canvas edge. This removes the connected checkerboard
# without punching holes into pale objects such as the teal plane or egg.
queue: deque[tuple[int, int]] = deque()
visited = bytearray(width * height)
for x in range(width):
queue.append((x, 0))
queue.append((x, height - 1))
for y in range(height):
queue.append((0, y))
queue.append((width - 1, y))
while queue:
x, y = queue.popleft()
offset = y * width + x
if visited[offset] or not is_background(x, y):
continue
visited[offset] = 1
if x:
queue.append((x - 1, y))
if x + 1 < width:
queue.append((x + 1, y))
if y:
queue.append((x, y - 1))
if y + 1 < height:
queue.append((x, y + 1))
for y in range(height):
for x in range(width):
red, green, blue, _ = pixels[x, y]
alpha = 0 if visited[y * width + x] else 255
pixels[x, y] = (red, green, blue, alpha)
bounds = rgba.getbbox()
if bounds:
rgba = rgba.crop(bounds)
return rgba
def copy_or_convert(source: Path, destination: Path) -> None:
destination.parent.mkdir(parents=True, exist_ok=True)
with Image.open(source) as image:
if has_real_alpha(image):
prepared = image.convert("RGBA")
bounds = prepared.getbbox()
if bounds:
prepared = prepared.crop(bounds)
prepared.thumbnail((720, 720), LANCZOS)
else:
prepared = remove_generated_checkerboard(image)
prepared.save(destination, optimize=True)
def main() -> None:
missing: list[str] = []
for output, source_name in ASSETS.items():
source = SOURCE_ROOT / source_name
destination = OUT_ROOT / output
if not source.exists():
missing.append(source_name)
continue
copy_or_convert(source, destination)
if missing:
raise SystemExit("Missing assets:\n" + "\n".join(missing))
if __name__ == "__main__":
main()