lili 86df18c4d8
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
feat(art): 山海风格版批次一——42 样张+产线三件真跑,创始人眼验拍锁锚开批产
矿彩国风锚 r1 定稿:《山海行纪》30 帧(hero×6/狰梼杌穷奇×15/法器×3/UI×2/场景/兽潮/Boss/拾取)+
《山海巡异录》12 张(卡面 3 稀有度×2 职业/敌兽 2/场景 2/结算 2);同名兽狰两款同谱面可辨=
「一条美术管线供两款」首检成立;clean-room 五条全程生效(prompt 零现代作品专名、兽形古籍演绎、
参考图仅自产、全量留痕 manifest.jsonl 含 12 张淘汰逐张原因)。
产线三件真跑演示:色键抠图+除杂(角落印章自动清)/图集 2 页 png 4006KB→webp 599KB(实测修正:
free-tex-packer 不直出 webp 补 PIL 转码)/字体子集 780 字→176KB woff2(LXGW OFL)。
成本良率实测:57 呼 ¥1.44(官方口径)/¥1.88(实测口径),首过率 34/42=81%,重摇≤4 轮填满;
工艺发现 7 条入批次 README(兽名不入 prompt/「鲛」prime 龙形/subject-ref 证伪/UI 伪字走程序排字等)。
创始人裁定(2026-07-07):锁锚开批产——两款按锚 r1 进 M3/M4 资产批产,主角跨帧漂移用
「定妆帧作参考图+关键姿态逐帧」工艺压,不换通道。台账:作战清单 M2 波全记账+_index 三行 M2 已交+设定档裁定戳。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:20:47 -07:00

59 lines
3.0 KiB
Python

#!/usr/bin/env python3
# 拼板图(contact sheet):批次样张 → 单张大图,创始人直接打开逐张眼验
# 用法: python3 contact-sheet.py <shots.json> <images根目录> <game> <输出.png> [列数] [标题]
# python3 contact-sheet.py --files <输出.png> <标题> <标签=路径>... (任意文件模式,供产线演示页)
# 排序按 shots.json 镜头单原序;每格附镜头 id 标签;中文标题用霞鹜文楷(OFL,scratchpad 依赖)。
import sys, os, json
from PIL import Image, ImageDraw, ImageFont
FONT_TTF = '/private/tmp/claude-501/-Users-lili-Project-games-development-ai/69248bd4-16ac-4906-aea1-4157edf7b10f/scratchpad/deps/LXGWWenKai-Regular.ttf'
def load_font(size):
try: return ImageFont.truetype(FONT_TTF, size)
except Exception: return ImageFont.load_default()
def make_sheet(items, out_path, cols, title, tile_w=360, tile_h=420):
label_h, title_h, gap = 30, 56, 8
rows = (len(items) + cols - 1) // cols
W = cols * (tile_w + gap) + gap
H = title_h + rows * (tile_h + label_h + gap) + gap
sheet = Image.new('RGB', (W, H), (30, 34, 32))
d = ImageDraw.Draw(sheet)
d.text((gap + 4, 12), title, fill=(232, 197, 106), font=load_font(30))
f_label = load_font(20)
for i, (label, path) in enumerate(items):
r, c = divmod(i, cols)
x = gap + c * (tile_w + gap)
y = title_h + r * (tile_h + label_h + gap)
try:
im = Image.open(path).convert('RGB')
except Exception as e:
d.text((x, y), f'{label}: 打开失败 {e}', fill=(200, 80, 80), font=f_label); continue
im.thumbnail((tile_w, tile_h))
ox, oy = x + (tile_w - im.width) // 2, y + (tile_h - im.height) // 2
sheet.paste(im, (ox, oy))
d.rectangle([x, y + tile_h, x + tile_w, y + tile_h + label_h], fill=(20, 23, 21))
d.text((x + 6, y + tile_h + 4), label, fill=(220, 220, 210), font=f_label)
sheet.save(out_path)
print(f'拼板完成:{out_path} ({W}x{H}, {len(items)} 格, {os.path.getsize(out_path)//1024}KB)')
if __name__ == '__main__':
if sys.argv[1] == '--files':
out, title = sys.argv[2], sys.argv[3]
items = [(kv.split('=', 1)[0], kv.split('=', 1)[1]) for kv in sys.argv[4:]]
make_sheet(items, out, cols=min(4, max(1, len(items))), title=title)
else:
shots_p, img_root, game, out = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
cols = int(sys.argv[5]) if len(sys.argv) > 5 else 6
title = sys.argv[6] if len(sys.argv) > 6 else f'风格版批次一 · {game}'
shots = [s for s in json.load(open(shots_p, encoding='utf-8'))['shots'] if s['game'] == game]
items = []
for s in shots:
hit = None
for ext in ('.jpg', '.jpeg', '.png', '.webp'):
p = os.path.join(img_root, game, s['id'] + ext)
if os.path.exists(p): hit = p; break
if hit: items.append((s['id'], hit))
else: print(f"警告:缺 {s['id']}")
make_sheet(items, out, cols=cols, title=title)