"""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()