首次提交:脑晴测 H5 项目
包含前端源码、预构建产物(dist/)、零依赖 Node.js 服务器(server.mjs)、 Cloudflare Worker 版本(worker/)、部署文档(DEPLOY.md)和交接文档。 技术总监可直接 clone 后运行 node server.mjs 启动,无需安装 React 或 build。 DEEPSEEK_API_KEY 通过环境变量注入,未纳入仓库。
This commit is contained in:
125
brain-h5/scripts/prepare-brain-assets.py
Normal file
125
brain-h5/scripts/prepare-brain-assets.py
Normal file
@@ -0,0 +1,125 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user