1仕組みと動作原理
最大の特徴は、「大量の画像をホワイトボードに並べる」処理を、ユーザーが作業中のボードを直接編集するのではなく、独立した scene ファイル(ピックトレイ)として構築することです。Job 1 全体で、N 枚の画像を等比率で縮小し、グリッドの中央に配置して files マップに base64 形式で埋め込みます。
The scene is a *pick tray*: open it in a second Excalidraw tab, select what you want, and copy-paste it onto your real board. It is never written into the board file itself — see references/paste_workflow.md for why that matters.
外部ライブラリに頼らず、ファイルヘッダーから直接画像サイズを解析します。PNG は IHDR の 8 バイトの幅・高さを読み取り、JPEG はセグメントをスキャンして SOF マーカーから幅・高さを取得します。
"""(width, height) in pixels for PNG or JPEG, read from the file's own header."""
書き込み後はディスクから再読み込み(リードバック)して自己検証を強制します。要素数、fileId と base64 コンテンツの sha1 の一致、元画像とのアスペクト比の一致、バウンディングボックスの重複有無を検証し、1 つでも不一致があれば SystemExit で終了します。
Deliberately re-reads from disk rather than checking the in-memory objects: the failure this catches is a bad write, and an in-memory assert cannot see one.
重複除外および既存要素の判定はファイル名ではなくコンテンツハッシュ(sha256)で行うため、リネームされた複製ファイルも正確に識別されます。
skip = {hashlib.sha256(p.read_bytes()).hexdigest() for p in exclude if p.exists()}
--template-from は「公式 JSON スキーマに image element 自身のフィールド定義が記載されていない」という欠落を解消します。ユーザー自身のボードから実際の image element をディープコピーしてフィールド構成を取得します。
for el in scene.get("elements", []): if el.get("type") == "image" and not el.get("isDeleted"): return copy.deepcopy(el)
受け渡し手順として「ファイルを開く(Open)」やドラッグ&ドロップではなく、意図的にクリップボードを採用しています。前者の 2 つはユーザーの現在のシーン全体を上書きしてしまうためです。
The clipboard carries elements *and* their embedded image payloads, so copy-paste merges instead of replacing:
Job 2 のスクリーンショットでコンテンツの欠落を防げる理由は、ソースコードから正規表現で slide id と fragmentCount を抽出すること、ハッシュ変更ではなくページごとの完全な page.goto を行うこと、そして innerText ではなく computed opacity でフラグメントが実際に表示されたかを判定することの 3 点にあります。
// Fragments that never revealed stay in the DOM at opacity 0, so any check // based on text content reports them as present while the picture is blank. // Measure what actually renders.
2 つのフラグメント進行モードは排他的とみなされ、スクリプト側で同時使用が禁止されています。--advance には pdf または keys のいずれかのみを指定できます。
console.error(`--advance must be "pdf" or "keys", got ${ADVANCE}`)
Job 3 の inspect_scene.py は読み取り専用のヘルスチェックを行います。要素タイプごとのカウント、埋め込みペイロードサイズ、fileId とコンテンツ sha1 の整合性、占有範囲(ユーザーがどこまでスクロールしてから貼り付けるべきか)を出力します。
# Excalidraw keys files by a content hash; when it holds, a # mismatch means the payload was swapped without rekeying.
split_scene.py は要素を均等分割した上で実際のチャンク数に基づいて命名し(「4-of-4 が存在しないのに 1-of-4 になる」状態を防止)、各チャンクが参照している files のみを保持します。そうしなければ分割による容量削減効果が得られないためです。
"files": {e["fileId"]: files[e["fileId"]] for e in part},
2コア機能
3外部依存関係
| Kind | Dependency / Endpoint |
|---|---|
| package | puppeteer(Job 2 无头浏览器,第三方 npm 包,需可被解析,skill 不负责安装) |
| cli | node(执行 shoot_deck.mjs) |
| cli | npx vite build(把 deck 从源码构建到 /tmp/deck-dist) |
| cli | python3 / python3 -m http.server(本地静态服务器) |
| network | http://127.0.0.1:8080(无头浏览器取页地址;注意 http.server 默认监听 0.0.0.0) |
| network | https://excalidraw.com(不是调用点:只作为生成 scene 的 source 字段字符串,以及文档里指向站点的手工操作说明) |
| network | https://docs.excalidraw.com/docs/codebase/json-schema(参考文献里引用的官方文档链接,非运行时代码) |
4推奨統制措置と判定根拠
- scripts/: ローカルスクリプトまたはコマンドの実行: excalidraw-use/scripts/build_scene.py (python3、標準ライブラリのみ)
- 网络端点: 外部ネットワークアクセスの宣言: http://127.0.0.1:8080 (ヘッドレスブラウザが /tmp/deck-dist を読み込むためのローカル静的サーバー、python3 -m http.server)
- 本地工作目录: ファイルの読み書きと成果物出力: <out>.excalidraw (build_scene.py により生成、base64 埋め込み画像を含む。親ディレクトリは自動作成)
5二重目の独立検証
- [ok] external_deps 每条的调用点是否真实存在 — puppeteer(shoot_deck.mjs 第 1 行 import)、node(shebang)、npx vite build 与 python3 -m http.server(SKILL.md Job 2 命令行)、127.0.0.1:8080(--url 实参)全部实读命中,非推测。
- [discrepancy] excalidraw.com 是否被当作网络端点误报 — 〔第二遍标注:第一遍易误报,已修正〕build_scene.py 与 split_scene.py 里的 https://excalidraw.com 只是写入 scene 的 "source" 字段值,脚本从不请求该域名;README/文档里的链接同理。已在 external_deps 中显式标注『不是调用点』,避免把字符串常量当成外发。
- [discrepancy] exec( 命中是否意味着命令执行 — 〔第二遍标注:扫描器误报,已澄清〕shoot_deck.mjs 的 const frag = /fragmentCount:\s*(\d+)/.exec(m[2]) 是 JavaScript RegExp.prototype.exec,属正则匹配,不执行进程。全目录无 child_process / spawn / execSync / subprocess。
- [ok] 『不写用户现场板子』是否只是口号 — 代码层:build_scene.py 只写 --out 指定路径,从不读入并回写用户的板子文件;文档层:SKILL.md 有独立小节『## Never write into their live board file』并给出两条独立理由(标签页不监听文件、磁盘文件通常旧于浏览器状态)。
- [ok] 写入后校验是否真做(不是文档承诺) — build_scene.py 的 verify() 在 build() 末尾被调用且从磁盘重读;split_scene.py 对每个分片做同样校验。两道都是可执行代码,失败即 SystemExit。
- [unlocatable] python3 -m http.server 绑定范围 — 〔第二遍标注:文档未声明,属解释器默认行为〕SKILL.md 只写 python3 -m http.server 8080,未写 --bind 127.0.0.1;http.server 的默认绑定是 0.0.0.0。这是 CPython 的既定行为而非本 skill 的代码,故标注为「文档未声明」并在 security.reason 中提示,不写成 skill 缺陷。
- [ok] Job 2 的 --advance 互斥是否代码强制 — shoot_deck.mjs 中 if (!['pdf','keys'].includes(ADVANCE)) 报错退出,且 query 只在 ADVANCE === 'pdf' 时拼 ?pdf=1;SKILL.md 有对应警告段,代码与文档一致。
- [ok] 元数据(license/stars/last_push) — 取自同 repo 同 commit 的池内既有值(MIT、1385★、2026-09-09T12:33:29Z);本次未联网复核 gh。
6結論
0c4ae44eb4c051d3…