Files
xsl d870a75a26 feat(new8/batch1): 两款新 RPG《不眠频率》radio +《回声地窟》dungeon 全套(多agent流水线:策划→资产→开发→测试→玩家R1→修复→R2)
- 程序章程 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(风暴根治/回声真实性/密度下限+呼吸层/导航最短路/菜单关闭反馈/金币档同账本)+ 实现核对登记
2026-09-02 18:52:37 +08:00

130 lines
4.7 KiB
Python
Raw Permalink 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.
"""《回声地窟》cover.png 生成:1080x1920,深色洞窟底 + 声波脉冲/等高线意象 + 标题。
可复现脚本(random.seed 固定)。PIL 探测顺序:系统 python3 → .venv-tts/bin/python,
都没有则 uv pip install pillow --python .venv-tts/bin/python(阿里云镜像)。
"""
import math
import os
import random
import subprocess
import sys
W, H = 1080, 1920
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "cover.png")
FONT_CANDIDATES = [
"/home/xsl/SoundGame/app/src/reflex/gen/NotoSansSC-Bold.ttf",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Bold.ttc",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc",
]
def find_font():
for p in FONT_CANDIDATES:
if os.path.exists(p):
return p
raise SystemExit("no CJK font found; add path to FONT_CANDIDATES")
def ensure_pil():
try:
import PIL # noqa: F401
return
except ImportError:
pass
venv_py = "/home/xsl/SoundGame/.venv-tts/bin/python"
if os.path.exists(venv_py):
subprocess.run([venv_py, "-m", "pip", "install", "pillow",
"--index-url", "https://mirrors.aliyun.com/pypi/simple/"], check=True)
else:
subprocess.run([sys.executable, "-m", "pip", "install", "pillow",
"--index-url", "https://mirrors.aliyun.com/pypi/simple/"], check=True)
ensure_pil()
from PIL import Image, ImageChops, ImageDraw, ImageFilter, ImageFont # noqa: E402
random.seed(42)
img = Image.new("RGB", (W, H))
px = img.load()
# 深色洞窟竖向渐变:上 #06070c -> 下 #14121c(近黑的岩壁,底部微暖)
top, bottom = (6, 7, 12), (20, 18, 28)
for y in range(H):
t = y / (H - 1)
for x in range(W):
px[x, y] = tuple(int(a + (b - a) * t) for a, b in zip(top, bottom))
d = ImageDraw.Draw(img)
# 等高线意象:一组以画面中心偏下为圆心的不规则闭合环(洞窟剖面)
cx, cy = W * 0.5, H * 0.46
for ring in range(9):
rr = 90 + ring * 78
pts = []
for i in range(72):
ang = i / 72 * math.tau
wob = 1 + 0.10 * math.sin(ang * 3 + ring) + 0.06 * math.cos(ang * 5 - ring * 2)
pts.append((cx + math.cos(ang) * rr * wob, cy + math.sin(ang) * rr * wob * 0.82))
shade = 26 + ring * 7
d.line(pts + [pts[0]], fill=(shade, shade + 4, shade + 14), width=3)
# 声波脉冲:从洞窟中心向外的一圈圈脉冲环(越远越淡 = 回声衰减)
pulse = Image.new("RGB", (W, H), (0, 0, 0))
pd = ImageDraw.Draw(pulse)
for ring in range(5):
rr = 60 + ring * 130
alpha = 90 - ring * 16
pd.ellipse([cx - rr, cy - rr * 0.82, cx + rr, cy + rr * 0.82],
outline=(alpha, alpha + 30, alpha + 60), width=6)
pulse = pulse.filter(ImageFilter.GaussianBlur(7))
img = ImageChops.add(img, pulse)
d = ImageDraw.Draw(img)
# 玩家光点 + 两侧回声亮斑(左右声道预 pan 意象)
d.ellipse([cx - 10, cy - 10, cx + 10, cy + 10], fill=(235, 240, 255))
glow = Image.new("RGB", (W, H), (0, 0, 0))
gd = ImageDraw.Draw(glow)
gd.ellipse([cx - 320, cy - 40, cx - 180, cy + 60], fill=(70, 110, 170)) # 左声道
gd.ellipse([cx + 190, cy - 120, cx + 330, cy - 10], fill=(70, 110, 170)) # 右声道
gd.ellipse([cx - 60, cy - 300, cx + 60, cy - 180], fill=(60, 100, 160)) # 正前远墙
glow = glow.filter(ImageFilter.GaussianBlur(30))
img = ImageChops.add(img, glow)
d = ImageDraw.Draw(img)
FONT = find_font()
def text_center(y, s, size, fill, glow_color=None):
f = ImageFont.truetype(FONT, size)
bbox = d.textbbox((0, 0), s, font=f)
x = (W - (bbox[2] - bbox[0])) / 2 - bbox[0]
if glow_color:
tg = Image.new("RGB", (W, H), (0, 0, 0))
ImageDraw.Draw(tg).text((x, y), s, font=f, fill=glow_color)
tg = tg.filter(ImageFilter.GaussianBlur(22))
img.paste(ImageChops.add(img.crop((0, 0, W, H)), tg).crop((0, 0, W, H)), (0, 0))
d.text((x, y), s, font=f, fill=fill)
else:
d.text((x, y), s, font=f, fill=fill)
title = "回 声 地 窟"
f = ImageFont.truetype(FONT, 210)
bbox = d.textbbox((0, 0), title, font=f)
tx = (W - (bbox[2] - bbox[0])) / 2 - bbox[0]
ty = 1210
tg = Image.new("RGB", (W, H), (0, 0, 0))
ImageDraw.Draw(tg).text((tx, ty), title, font=f, fill=(40, 90, 150))
tg = tg.filter(ImageFilter.GaussianBlur(20))
img = ImageChops.add(img, tg)
d = ImageDraw.Draw(img)
d.text((tx, ty), title, font=f, fill=(230, 240, 252))
text_center(1490, "ECHO CAVERN", 42, (96, 128, 176))
text_center(1600, "伸手不见五指,声音就是你的眼睛", 56, (206, 220, 244))
text_center(1700, "单击脉冲回声看路 · 按住前进 · 左右滑转身", 40, (128, 142, 170))
text_center(1770, "双击再听 · 上滑状态 · 下滑菜单", 40, (128, 142, 170))
img.save(OUT)
print("saved", os.path.abspath(OUT), img.size)