包含前端源码、预构建产物(dist/)、零依赖 Node.js 服务器(server.mjs)、 Cloudflare Worker 版本(worker/)、部署文档(DEPLOY.md)和交接文档。 技术总监可直接 clone 后运行 node server.mjs 启动,无需安装 React 或 build。 DEEPSEEK_API_KEY 通过环境变量注入,未纳入仓库。
126 lines
4.5 KiB
Python
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()
|