#!/usr/bin/env python # -*- coding: utf-8 -*- """ lint_frames.py —— 场景帧静态体检(渲染前跑,比渲完全片再发现问题便宜得多)。 检查项(全部来自 references/frame-contract.md 的八条契约 + 已知踩坑): 1. 画布:html,body 是否声明 1920×1080(或 project.json 的尺寸) 2. 外部字体 / CDN:、fonts.googleapis、@import url(http 3. 色值字面量:#RRGGBB / rgb() / rgba() 出现在 CSS 里(theme.css 变量之外) —— 例外:纯黑/纯白阴影 rgba(0,0,0,x) 允许(阴影不是"配色") 4. 时间轴注册:window.__tl = (或 window.__timelines) 5. GSAP 本地引用:../assets/gsap.min.js(禁 CDN) 6. 墙钟逻辑:setInterval / setTimeout(数字) / requestAnimationFrame 计数 7. CSS transition 入场 8. B() 兜底:B('x') || 数字 9. 字幕带禁区:CSS 里 bottom < 176px 的规则且带内容属性(启发式,转人工确认) 10. 字体栈:必须含中文字体族(Microsoft YaHei / PingFang 等) 用法: python lint_frames.py --project . # 全量 python lint_frames.py --project . --only hook,decision """ from __future__ import annotations import argparse import json import os import re import sys HEX = re.compile(r"#[0-9a-fA-F]{3,8}\b") RGBA = re.compile(r"\brgba?\([^)]*\)") FONT_LINK = re.compile(r"]+href\s*=\s*[\"']https?://", re.I) IMPORT_HTTP = re.compile(r"@import\s+url\(\s*[\"']?https?://", re.I) GSAP_CDN = re.compile(r"]+src\s*=\s*[\"']https?://", re.I) B_FALLBACK = re.compile(r"B\([^)]*\)\s*\|\|") WALLCLOCK = re.compile(r"setInterval\s*\(|setTimeout\s*\(\s*[a-zA-Z_]|requestAnimationFrame\s*\(\s*(?!function)") CSS_TRANSITION = re.compile(r"transition\s*:", re.I) BOTTOM_RULE = re.compile(r"(? list[str]: src = open(path, encoding="utf-8").read() name = os.path.basename(path) out: list[str] = [] # 1. 画布(封面有自己的规格:16:9 版 1920×1080 / 3:4 版 1440×1080) if is_cover: if not re.search(r"width\s*:\s*\d+px", src) or not re.search(r"height\s*:\s*\d+px", src): out.append("封面:未见 width/height 像素声明") elif not re.search(rf"width\s*:\s*{want_w}px", src) or not re.search(rf"height\s*:\s*{want_h}px", src): out.append(f"画布:未见 width:{want_w}px / height:{want_h}px 声明") # 2. 外部字体 / 远程资源 if FONT_LINK.search(src): out.append("外部 (字体/样式)→ 违反契约 2(渲染器 5s 字体硬顶)") if IMPORT_HTTP.search(src): out.append("@import url(http…) → 违反契约 2") if GSAP_CDN.search(src): out.append("