First public release of the html-explainer skill: deterministic HTML-to-MP4 explainer video pipeline for AI coding agents. Additions since v1.2.1: - scripts/lint_frames.py: pre-render audit of the eight frame-contract rules - scripts/peek_frame.mjs: single-scene screenshot preview (seconds, not minutes) - scripts/check_integrity.py: version/catalog/count consistency + stray-CDN scan - Bilingual README (README.md Chinese default, README.en.md English) - LICENSE (MIT), THIRD_PARTY_NOTICES.md, licenses/Apache-2.0.txt - CHANGELOG.md, CONTRIBUTING.md, .gitignore, .gitattributes - GitHub CI workflow and issue/PR templates Fixes and docs: - document lint_frames.py and peek_frame.mjs, previously absent from the file table - add the lint stage to the pipeline order - remove references to private client projects so the skill can ship publicly - record the full upstream attribution chain for the style catalog - package_skill.py now excludes repo-hygiene files and keeps licence files
121 lines
4.5 KiB
Python
121 lines
4.5 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
package_skill.py —— 把 html-explainer 打包成可带到任意终端的 zip。
|
||
|
||
设计取舍:
|
||
· **排除 node/node_modules/**(14MB,且平台相关)—— setup_env.sh --install 会重装。
|
||
这样 zip 只有几十 KB,纯文本 + 一份 GSAP。
|
||
· **保留 assets/gsap.min.js**(72KB,离线必需,体积可接受)。
|
||
· **保留 README / LICENSE / THIRD_PARTY_NOTICES.md / licenses/** —— 再分发时必须随包
|
||
带上这些文件(Apache-2.0 §4 要求收到衍生作品的人同时收到许可证副本)。
|
||
· **排除仓库卫生文件**(.github/、.gitignore、.gitattributes)—— 它们属于 GitHub
|
||
仓库,不属于可移植的技能包。
|
||
· 排除运行时产物(out/、audio/、*.beats.js、__pycache__、诊断脚本)。
|
||
|
||
用法:
|
||
python package_skill.py # 产出 dist/html-explainer-vX.Y.zip
|
||
python package_skill.py --with-deps # 连 node_modules 一起打(完全离线可跑,~14MB)
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import os
|
||
import re
|
||
import sys
|
||
import zipfile
|
||
from datetime import date
|
||
|
||
SKILL_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||
|
||
# 目录名匹配即整棵排除
|
||
EXCLUDE_DIRS = {
|
||
"node_modules", "__pycache__", "dist", "out", "audio", ".git",
|
||
".pytest_cache", "qc_frames",
|
||
".github", # 仓库卫生:CI / issue 模板不属于可移植技能包
|
||
"render", "research", "script", # 项目级产物(防在技能目录内建过 demo)
|
||
}
|
||
# 文件名 / 后缀排除
|
||
EXCLUDE_SUFFIX = (".pyc", ".pyo", ".beats.js", ".mp4", ".mp3", ".wav", ".log")
|
||
EXCLUDE_NAMES = {
|
||
"package-lock.json", ".DS_Store", "Thumbs.db", "qc_sheet.jpg", "qc_report.md",
|
||
".gitignore", ".gitattributes",
|
||
}
|
||
|
||
|
||
def version() -> str:
|
||
md = os.path.join(SKILL_ROOT, "SKILL.md")
|
||
try:
|
||
txt = open(md, encoding="utf-8").read()
|
||
m = re.search(r"^version:\s*(\S+)", txt, re.M)
|
||
if m:
|
||
return m.group(1).strip().strip('"\'')
|
||
except OSError:
|
||
pass
|
||
return "1.0.0"
|
||
|
||
|
||
def should_skip(rel: str, with_deps: bool) -> bool:
|
||
parts = rel.replace("\\", "/").split("/")
|
||
for p in parts[:-1]:
|
||
if p in EXCLUDE_DIRS:
|
||
if with_deps and p == "node_modules":
|
||
continue
|
||
return True
|
||
name = parts[-1]
|
||
if name in EXCLUDE_NAMES:
|
||
return True
|
||
if name.endswith(EXCLUDE_SUFFIX):
|
||
return True
|
||
# 临时诊断脚本(下划线开头 .js/.mjs/.py,非技能正式脚本)
|
||
if parts[0] == "scripts" and name.startswith("_"):
|
||
return True
|
||
return False
|
||
|
||
|
||
def main() -> int:
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--with-deps", action="store_true",
|
||
help="把 node/node_modules 一起打包(离线可跑,体积 ~14MB)")
|
||
ap.add_argument("--out", default=None)
|
||
args = ap.parse_args()
|
||
|
||
ver = version()
|
||
dist = os.path.join(SKILL_ROOT, "dist")
|
||
os.makedirs(dist, exist_ok=True)
|
||
tag = "-full" if args.with_deps else ""
|
||
out = args.out or os.path.join(dist, f"html-explainer-v{ver}{tag}.zip")
|
||
|
||
count = 0
|
||
total = 0
|
||
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z:
|
||
for root, dirs, files in os.walk(SKILL_ROOT):
|
||
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS or
|
||
(args.with_deps and d == "node_modules")]
|
||
for f in files:
|
||
full = os.path.join(root, f)
|
||
rel = os.path.relpath(full, SKILL_ROOT)
|
||
if should_skip(rel, args.with_deps):
|
||
continue
|
||
arc = os.path.join("html-explainer", rel.replace("\\", "/"))
|
||
z.write(full, arc)
|
||
count += 1
|
||
total += os.path.getsize(full)
|
||
|
||
size = os.path.getsize(out)
|
||
print(f"✓ 打包完成:{out}")
|
||
print(f" 文件 {count} 个 · 源 {total / 1024:.0f}KB · zip {size / 1024:.0f}KB"
|
||
f"(压缩率 {100 - size * 100 / max(total, 1):.0f}%)")
|
||
if not args.with_deps:
|
||
print(" 注:未含 node/node_modules —— 目标机首次跑 `bash setup_env.sh --install` 装齐")
|
||
print()
|
||
print(" 带到新终端后:")
|
||
print(" 1. 解压到 ~/.workbuddy/skills/ (或任意位置,脚本按自身路径定位)")
|
||
print(" 2. bash setup_env.sh --install")
|
||
print(" 3. python scripts/new_project.py <name> && 编辑 narration.json …")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|