Files
SoundGame/web/keeper/gen/gen_cover.py
T
xsl 4b39ae2e49 feat(new8/batch3-4): cipher/teahouse/keeper/bridge 全套交付——new8 八款全部可交付收官
四泳道并行全流水线(资产→开发→测试→玩家迭代闭环),双双 R1-R2 连续达标裁定可交付:
- cipher《暗码》223 断言:摩尔斯解码嵌叙事/四结局矩阵/暴露度逐NIGHT验算/三层兜底;R2 三修(链首标记音/预警震/结局计数句+4句补录)4/4 全满分档
- teahouse《深夜茶馆》205 断言:泡茶时机窗/经济逐晚对账/多晚弧线;连续 2 轮 4/4 愿意玩 P0=0
- keeper《守塔人》257 断言:时空签名教学-兑现/插叙触发回返/trust×truth 档位/4结局;R2 F1/F2 坐实(震型豁免/首播跳句锁)4/4
- bridge《星环舰长》336 断言:巡检节奏逐毫秒/危机×系统态矩阵/仲裁7场景/忠诚差分/评级验算;R1 当场修F1-F4、R2 F5 菜单顺延保护 4/4 上行

公共:index.html 四卡片+profiles.js 四桶+check_all_assets 登记(预接线)、00-program 八款进度账收账、
gen_voice_dual 四游戏段、README/HANDOVER 阵容更新(11 款)、voice_text.json 四游戏键、mureka 台账(累计$1.26/$2.00)

验证:四游戏 playtest 223/205/257/336 全绿 + guide 122/122 + 全项目 15 游戏资产终检绿
遗留:keeper 三代人声滤波人耳试听(最高优先,ASR 对滤波句失明)、bridge 音频 32MB 超软指引 2MB(压缩候选)
2026-09-14 05:48:13 +08:00

126 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""web/keeper 封面:1080x1920,孤岛灯塔主题——墨黑风暴海、孤塔双道光束、
雨丝与闪电微光,竖排标题《守塔人》。可复现脚本(固定 seed,无外部网络依赖)。
用法:python3 web/keeper/gen/gen_cover.py
(或 .venv-tts/bin/python web/keeper/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(2114) # 固定 seed:可复现
img = Image.new("RGB", (W, H), (6, 10, 18))
d = ImageDraw.Draw(img)
# 背景竖向渐变:墨黑风暴 → 海面微光(灯塔下方最亮)
for y in range(H):
t = y / H
glow = max(0.0, 1.0 - abs(t - 0.62) * 3.2) * 26
d.line([(0, y), (W, y)],
fill=(int(6 + 8 * t), int(10 + 12 * t), int(18 + 22 * t + glow)))
# 雨丝(风暴签名)
rain = Image.new("RGBA", (W, H), (0, 0, 0, 0))
rd = ImageDraw.Draw(rain)
for _ in range(340):
x = rng.randint(-40, W + 40)
y = rng.randint(0, H)
ln = rng.randint(26, 64)
rd.line([(x, y), (x - ln // 4, y + ln)],
fill=(150, 175, 205, rng.randint(10, 30)), width=1)
img.paste(rain, (0, 0), rain)
d = ImageDraw.Draw(img)
# 灯塔(中下偏左):塔身条纹 + 灯室
cx, base_y = W // 2 - 60, 1250
top_y = 620
for i in range(7):
y0 = top_y + i * 90
y1 = y0 + 90
w0 = 46 + i * 7
w1 = 46 + (i + 1) * 7
col = (214, 218, 224) if i % 2 == 0 else (158, 42, 43)
d.polygon([(cx - w0, y0), (cx + w0, y0), (cx + w1, y1), (cx - w1, y1)], fill=col)
d.rectangle([cx - 40, top_y - 46, cx + 40, top_y], fill=(30, 34, 44))
d.rectangle([cx - 26, top_y - 38, cx + 26, top_y - 8], fill=(255, 214, 120))
d.polygon([(cx - 46, top_y - 46), (cx + 46, top_y - 46), (cx, top_y - 84)], fill=(24, 28, 38))
# 双道光束(旋转灯扫过雨夜,暖黄、模糊压暗)
beam = Image.new("RGBA", (W, H), (0, 0, 0, 0))
bd = ImageDraw.Draw(beam)
for ang, alpha in ((-18, 60), (162, 44)):
a = math.radians(ang)
bx, by = cx, top_y - 27
L = 950
ex, ey = bx + L * math.cos(a), by - L * math.sin(a) * 0.42
spread = 90
perp = (math.cos(a + math.pi / 2), -math.sin(a + math.pi / 2) * 0.42)
bd.polygon([(bx, by),
(ex + perp[0] * spread, ey + perp[1] * spread),
(ex - perp[0] * spread, ey - perp[1] * spread)],
fill=(255, 206, 110, alpha))
beam = beam.filter(ImageFilter.GaussianBlur(22))
img.paste(beam, (0, 0), beam)
d = ImageDraw.Draw(img)
# 海面(塔基下):暗色波纹横线
sea = Image.new("RGBA", (W, H), (0, 0, 0, 0))
sd = ImageDraw.Draw(sea)
for i in range(60):
y = base_y + rng.randint(-20, 330)
x0 = rng.randint(-60, W - 200)
ln = rng.randint(120, 400)
sd.line([(x0, y), (x0 + ln, y)], fill=(90, 120, 150, rng.randint(16, 44)), width=2)
img.paste(sea, (0, 0), sea)
d = ImageDraw.Draw(img)
# 标题与副题
f_title = load_font(180)
f_sub = load_font(52)
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(1430, "守塔人", f_title, (238, 242, 250), spacing=16)
ctext(1700, "风暴夜 · 一盏灯 · 一条频道", f_sub, (146, 162, 186))
# 手势图例(极简两行)
hints = ["单击 继续 / 跳句", "左右滑 抉择", "上滑 状态", "下滑 菜单", "双击 重听"]
for i, s in enumerate(hints):
col, row = i % 3, i // 3
d.text((110 + col * 300, 1800 + row * 60), s, font=f_small, fill=(104, 124, 152))
ctext(1180, "唱针落下是过去,风声回来是现在。", f_small, (96, 112, 140))
img.save(OUT)
print(OUT, img.size)
if __name__ == "__main__":
main()