Files
SoundGame/web/detective/gen/gen_cover.py
T
xsl 31372615b2 feat(new8/batch2): 《深海回音》abyss +《雨夜专案》detective 全套交付(多agent流水线:策划→资产→开发→测试→真资产复跑)
- abyss:game.js 242 断言全绿(剧本A-F六路径/氧气门三形态/sonarGate仲裁/结算死窗/双轨看门狗/radio 8项修复继承);资产 148句+11key变体(电台滤波带内能量47-62%→69-80%)
- detective:game.js 265 断言真资产下全绿(矩阵不变式独立重算/指证全路径/评级12组注入/VERDICT死窗/二周目变体);真资产复跑回炉3处实现修复(无进展轨墙钟化/菜单暂停记账/停止播报真停声——mock时长掩盖的正确性bug)
- 资产:TTS 539条×2格式、Mureka 7首 $0.21(累计$1.08/$2.00)、SYN12+SA3 19件(QC抓出1例AAC过冲并修复)、封面×2、check_all_assets 10游戏全绿(abyss 298/298、detective 279/279)
- 平台接线:大厅双卡片、BUCKETS 增 abyss/detective 桶(H4 断言更新 10 游戏)、voice_text.json 10游戏991句
- 回归:14 套件全绿(10 playtest+guide+engine-voice+engine-input+api-smoke 12/12)
- 设计偏差登记:abyss 7条/detective 9条(全为设计内部冲突按承诺清单/归因真话律取优)
2026-09-10 01:40:31 +08:00

116 lines
4.5 KiB
Python

"""web/detective 封面:1080x1920,雨夜推理主题——墨蓝夜雨、一盏路灯暖光、
雨丝竖线与居中标题《雨夜专案》。可复现脚本(固定 seed,无外部网络依赖)。
用法(PIL 探测顺序:系统 python3 → .venv-tts → 装 pillow 后任一):
python3 web/detective/gen/gen_cover.py
"""
import math
import os
import random
from PIL import Image, ImageDraw, ImageFilter, ImageFont
W, H = 1080, 1920
FONT_CANDIDATES = [
"/home/xsl/SoundGame/web/rhythm/gen/NotoSansSC-Bold.ttf",
"/home/xsl/SoundGame/app/src/reflex/gen/NotoSansSC-Bold.ttf",
]
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "cover.png")
def load_font(size):
for p in FONT_CANDIDATES:
if os.path.exists(p):
return ImageFont.truetype(p, size)
raise SystemExit("未找到中文字体,请放一份 NotoSansSC-Bold.ttf 到 web/rhythm/gen/")
def main():
rng = random.Random(918) # 固定 seed:可复现
img = Image.new("RGB", (W, H), (10, 12, 20))
d = ImageDraw.Draw(img)
# 背景竖向渐变:夜空墨蓝 -> 街面冷灰(雨夜街道)
for y in range(H):
t = y / H
d.line([(0, y), (W, y)],
fill=(int(14 + 10 * t), int(17 + 12 * t), int(26 + 18 * t)))
# 一盏路灯暖光(左上,光晕压暗)——雨夜里唯一的暖色
lx, ly = 250, 380
lamp = Image.new("RGBA", (W, H), (0, 0, 0, 0))
ld = ImageDraw.Draw(lamp)
for r, alpha in [(420, 26), (300, 40), (190, 58), (110, 80)]:
ld.ellipse([lx - r, ly - r, lx + r, ly + r], fill=(253, 230, 138, alpha))
lamp = lamp.filter(ImageFilter.GaussianBlur(14))
img.paste(lamp, (0, 0), lamp)
d = ImageDraw.Draw(img)
d.ellipse([lx - 26, ly - 26, lx + 26, ly + 26], fill=(255, 244, 214))
# 雨丝:斜细线两景深(远淡近亮),风斜角 ~8°
rain = Image.new("RGBA", (W, H), (0, 0, 0, 0))
rd = ImageDraw.Draw(rain)
for _ in range(90): # 远景雨
x = rng.randint(0, W + 200)
y = rng.randint(0, H)
ln = rng.randint(40, 90)
rd.line([(x, y), (x - ln * 0.14, y + ln)],
fill=(148, 163, 184, rng.randint(16, 34)), width=1)
for _ in range(60): # 近景雨
x = rng.randint(0, W + 200)
y = rng.randint(0, H)
ln = rng.randint(90, 160)
rd.line([(x, y), (x - ln * 0.14, y + ln)],
fill=(203, 213, 225, rng.randint(28, 52)), width=2)
img.paste(rain, (0, 0), rain)
d = ImageDraw.Draw(img)
# 中央:案卷卷宗剪影(翻开的两页,暗金色边线)——「卷宗六份」的具象
cx, cy = W // 2, 640
doc = Image.new("RGBA", (W, H), (0, 0, 0, 0))
ddoc = ImageDraw.Draw(doc)
ddoc.polygon([(cx - 40, cy - 120), (cx - 250, cy - 60), (cx - 250, cy + 190), (cx - 40, cy + 130)],
outline=(120, 113, 108, 220), width=4)
ddoc.polygon([(cx + 40, cy - 120), (cx + 250, cy - 60), (cx + 250, cy + 190), (cx + 40, cy + 130)],
outline=(120, 113, 108, 220), width=4)
# 页面行文线(证词的横线,右页夹一条对不上的红线)
for i in range(5):
yy = cy - 26 + i * 40
ddoc.line([(cx - 215, yy - 28), (cx - 75, yy - 46)], fill=(148, 163, 184, 120), width=3)
ddoc.line([(cx + 75, yy - 46), (cx + 215, yy - 28)], fill=(148, 163, 184, 120), width=3)
ddoc.line([(cx + 92, cy + 62), (cx + 208, cy + 80)], fill=(248, 113, 113, 200), width=4)
img.paste(doc, (0, 0), doc)
d = ImageDraw.Draw(img)
# 标题(居中)与副题
f_title = load_font(172)
f_sub = load_font(54)
f_small = load_font(38)
def ctext(y, s, font, fill, spacing=8):
bbox = d.textbbox((0, 0), s, font=font)
w = bbox[2] - bbox[0]
x = (W - w) / 2
for ch in s:
d.text((x, y), ch, font=font, fill=fill)
bb = d.textbbox((0, 0), ch, font=font)
x += (bb[2] - bb[0]) + spacing
ctext(1010, "雨夜专案", f_title, (237, 233, 254), spacing=16)
ctext(1262, "证词会撒谎,证据不会", f_sub, (148, 163, 184))
# 手势图例(极简两行)
hints = ["左右滑 换卷宗", "单击 听取", "长按 指证", "上滑 进度", "下滑 菜单"]
for i, s in enumerate(hints):
col, row = i % 3, i // 3
d.text((110 + col * 300, 1420 + row * 76), s, font=f_small, fill=(104, 122, 152))
ctext(1700, "翻听六份卷宗,指出矛盾,锁定真凶。", f_small, (86, 102, 130))
img.save(OUT)
print(OUT, img.size)
if __name__ == "__main__":
main()