- 程序章程 research/new8/00-program.md(8款概念/回合协议/验收门/进度账) - 设计:docs/design/15-radio.md+voice-ids(132句,6状态9抉择4结局,电话滤波资产侧带通)/16-dungeon.md+voice-ids(125句,脉冲回声/7层seed生成/三档战斗/元进度遗产) - 资产:TTS 970文件(GPU flock串行)+ Mureka BGM 8首 $0.24(台账 mureka_log.md)+ 程序合成 sfx(含预pan侧墙/开口、phone_ring铃声、amb_cave呼吸层、turn_tick)+ 终检 check_all_assets 8游戏全绿 - radio:game.js+playtest 221断言(4结局矩阵/pendingChoice盲玩公平/RING两级看门狗/菜单循环/存档schema) - dungeon:game.js+playtest 1610断言(seed全网格回声一致性/学期零死/格挡80%/死亡归因真话/元进度/菜单冻结/上滑5段状态链) - 引擎修复:audio.js _playVoice 加载竞态补杀 token(R1真机71分钟语音风暴根因)+ 同id 3s窗口≥5次熔断保险丝 + gesture.js 长按boost事件门(+66行,9套件回归全绿) - 大厅:新增 radio/dungeon 卡片 + GAMES 数组 - harness:sessions/radio.mjs+dungeon.mjs(主线+压力双剧本)+ transcribe 注册 + voice_text.json 刷新(8游戏704句)+ 13号文档 §20 登记 - R1 存档:4人设评审×2游戏 + 真机试玩报告×2(radio 5/5愿意玩;dungeon 0/4→确诊风暴)+ synthesis×2 + 转写8份 - R2 修复:radio F1-F5(看门狗补挂/rep_low改词重录/真铃声/ASK续玩语义/链末跳过保护)+ dungeon D0-1..D1-6(风暴根治/回声真实性/密度下限+呼吸层/导航最短路/菜单关闭反馈/金币档同账本)+ 实现核对登记
100 lines
3.7 KiB
Python
100 lines
3.7 KiB
Python
"""web/radio 封面:1080x1920,深夜电台主题——深蓝夜色底、红灯「ON AIR」、
|
||
声波弧线与竖排标题《不眠频率》。可复现脚本(固定 seed,无外部网络依赖)。
|
||
|
||
用法:python3 web/radio/gen/gen_cover.py
|
||
(或 .venv-tts/bin/python web/radio/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(915) # 固定 seed:可复现
|
||
img = Image.new("RGB", (W, H), (8, 10, 24))
|
||
d = ImageDraw.Draw(img)
|
||
|
||
# 背景竖向微渐变:深夜蓝 → 微亮天际(黎明前的电台夜班)
|
||
for y in range(H):
|
||
t = y / H
|
||
d.line([(0, y), (W, y)],
|
||
fill=(int(8 + 10 * t), int(10 + 12 * t), int(24 + 26 * t)))
|
||
|
||
# 远景声波弧线(电台频率的具象,模糊压暗做背景)
|
||
wave = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
||
wd = ImageDraw.Draw(wave)
|
||
palette = [(94, 234, 212), (129, 140, 248), (244, 114, 182)]
|
||
for i in range(22):
|
||
cx, cy = rng.randint(0, W), rng.randint(0, H)
|
||
r = rng.randint(80, 360)
|
||
col = palette[i % len(palette)]
|
||
wd.ellipse([cx - r, cy - r, cx + r, cy + r],
|
||
outline=col + (rng.randint(8, 26),), width=rng.randint(2, 4))
|
||
wave = wave.filter(ImageFilter.GaussianBlur(1.6))
|
||
img.paste(wave, (0, 0), wave)
|
||
d = ImageDraw.Draw(img)
|
||
|
||
# 中央:红色「ON AIR」指示灯 + 同心圆话筒波纹
|
||
cx, cy = W // 2, 620
|
||
for i, (r, alpha) in enumerate([(300, 46), (224, 78), (152, 118)]):
|
||
ring = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
||
rd = ImageDraw.Draw(ring)
|
||
rd.ellipse([cx - r, cy - r, cx + r, cy + r],
|
||
outline=(248, 113, 113, alpha), width=8 - i * 2)
|
||
img.paste(ring, (0, 0), ring)
|
||
d = ImageDraw.Draw(img)
|
||
d.ellipse([cx - 64, cy - 64, cx + 64, cy + 64], fill=(220, 38, 38))
|
||
d.ellipse([cx - 30, cy - 30, cx + 30, cy + 30], fill=(252, 165, 165))
|
||
f_lamp = load_font(52)
|
||
bbox = d.textbbox((0, 0), "ON AIR", font=f_lamp)
|
||
d.text((cx - (bbox[2] - bbox[0]) / 2 - bbox[0], cy - (bbox[3] - bbox[1]) / 2 - bbox[1] - 210),
|
||
"ON AIR", font=f_lamp, fill=(248, 113, 113))
|
||
|
||
# 标题(居中)与副题
|
||
f_title = load_font(168)
|
||
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(1020, "不眠频率", f_title, (240, 244, 255), spacing=14)
|
||
ctext(1260, "深夜电台热线 · 接住五个陌生人的夜晚", 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=(110, 130, 160))
|
||
|
||
ctext(1700, "午夜零点,热线灯亮。你说什么,由你决定。", f_small, (90, 105, 135))
|
||
|
||
img.save(OUT)
|
||
print(OUT, img.size)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|