akari-video 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/migrate-command.mjs +11 -2
- package/vendor/.akari-capability-sources.json +3 -0
- package/vendor/docs/contract-2026-07-25-project-structure-v0.md +17 -0
- package/vendor/docs/contract-2026-08-02-preview-parity.md +39 -0
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/analysis-report/package.json +4 -8
- package/vendor/packages/analysis-report/render-analysis-report.mjs +544 -0
- package/vendor/packages/analysis-report/template.html +2023 -0
- package/vendor/packages/edit-lint/src/edit-lint.mjs +20 -0
- package/vendor/packages/edit-store/lib/caption-display.d.ts +10 -0
- package/vendor/packages/edit-store/lib/caption-display.js +57 -0
- package/vendor/packages/edit-store/lib/index.d.ts +1 -0
- package/vendor/packages/edit-store/lib/index.js +1 -0
- package/vendor/packages/edit-store/lib/internal-model.js +20 -8
- package/vendor/packages/edit-store/lib/migrate/index.d.ts +2 -0
- package/vendor/packages/edit-store/lib/migrate/index.js +32 -15
- package/vendor/packages/edit-store/lib/transition-visual.d.ts +23 -0
- package/vendor/packages/edit-store/lib/transition-visual.js +141 -0
- package/vendor/packages/edit-store/lib/webview-kernel.d.ts +2 -0
- package/vendor/packages/edit-store/lib/webview-kernel.js +139 -0
- package/vendor/packages/frame-engine/README.ja.md +24 -0
- package/vendor/packages/frame-engine/README.md +24 -0
- package/vendor/packages/frame-engine/package.json +27 -0
- package/vendor/packages/preview-engine/README.md +38 -0
- package/vendor/packages/preview-server/package.json +1 -1
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// analysis.json(事実層・複数可)+ interpretation.json(解釈層・プロジェクト単位 1 ファイル)から
|
|
4
|
+
// 読み取り専用の分析レポート HTML を生成する。
|
|
5
|
+
//
|
|
6
|
+
// テンプレート(template.html)は不変の UI 正本で、このスクリプトの役割は
|
|
7
|
+
// (1) interpretation.json を validate-interpretation.mjs で検証する
|
|
8
|
+
// (2) analysis.json 群を構造的に検証する
|
|
9
|
+
// (3) keyframe 画像の実在確認(存在すれば相対パスを解決、無ければ null — テンプレ側が
|
|
10
|
+
// note チップへ縮退する)
|
|
11
|
+
// (4) 生データをそのまま <script type="application/json"> ブロックへ束ねて埋め込む
|
|
12
|
+
// だけであり、章の折りたたみ・空状態文言・バッジ表示などの描画ロジックは一切持たない
|
|
13
|
+
// (それは template.html の役割)。interpretation.json の arc はここでも束ねてそのまま
|
|
14
|
+
// 埋め込む(レポート表示からは除外されるが、schema・データは不変 — 2026-07-22 改訂)。
|
|
15
|
+
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
|
|
21
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const templatePath = resolve(here, "template.html");
|
|
23
|
+
const validateInterpretationBin = resolve(
|
|
24
|
+
here,
|
|
25
|
+
"../schemas/bin/validate-interpretation.mjs",
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const DATA_PLACEHOLDER = '{"__AKARI_ANALYSIS_REPORT_DATA__": true}';
|
|
29
|
+
const BLOCKS_PLACEHOLDER = '{"__AKARI_ANALYSIS_REPORT_BLOCKS__": true}';
|
|
30
|
+
|
|
31
|
+
function usage() {
|
|
32
|
+
return [
|
|
33
|
+
"使い方:",
|
|
34
|
+
" node render-analysis-report.mjs --analysis <ref>=<path> [--analysis <ref>=<path> ...] --interpretation <path> --out <report.html>",
|
|
35
|
+
"",
|
|
36
|
+
" --analysis の正式形は <ref>=<path>(ref は interpretation.assets[].ref を明示する)。",
|
|
37
|
+
" 素の <path> のみの指定も許容するが、inputs.analyses[].path と basename / suffix で",
|
|
38
|
+
" 一意に照合できる場合に限る(不一致・曖昧は即エラー。順序へのフォールバックはしない)。",
|
|
39
|
+
" どちらの形でも、与えた path は当該 ref の inputs.analyses[].path と対応しているかを",
|
|
40
|
+
" 照合する(2026-07-22 A3.2 の取り違え実証を受けた FK クロスチェック)。",
|
|
41
|
+
" interpretation.assets[] の全 ref に対応する --analysis が過不足なく必要。",
|
|
42
|
+
].join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseArgs(argv) {
|
|
46
|
+
const analysisPaths = [];
|
|
47
|
+
let interpretationPath = null;
|
|
48
|
+
let outPath = null;
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
51
|
+
const arg = argv[i];
|
|
52
|
+
if (arg === "--help" || arg === "-h") {
|
|
53
|
+
return { help: true };
|
|
54
|
+
}
|
|
55
|
+
if (arg === "--analysis") {
|
|
56
|
+
const value = argv[i + 1];
|
|
57
|
+
if (value === undefined) throw new Error("--analysis には値が必要です");
|
|
58
|
+
analysisPaths.push(value);
|
|
59
|
+
i += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (arg === "--interpretation") {
|
|
63
|
+
const value = argv[i + 1];
|
|
64
|
+
if (value === undefined) throw new Error("--interpretation には値が必要です");
|
|
65
|
+
interpretationPath = value;
|
|
66
|
+
i += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (arg === "--out") {
|
|
70
|
+
const value = argv[i + 1];
|
|
71
|
+
if (value === undefined) throw new Error("--out には値が必要です");
|
|
72
|
+
outPath = value;
|
|
73
|
+
i += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`未知の引数です: ${arg}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (analysisPaths.length === 0) throw new Error("--analysis を 1 件以上指定してください");
|
|
80
|
+
if (!interpretationPath) throw new Error("--interpretation は必須です");
|
|
81
|
+
if (!outPath) throw new Error("--out は必須です");
|
|
82
|
+
|
|
83
|
+
return { help: false, analysisPaths, interpretationPath, outPath };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function fail(message) {
|
|
87
|
+
console.error(message);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isRecord(value) {
|
|
92
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isNonEmptyString(value) {
|
|
96
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readJson(path, label) {
|
|
100
|
+
let text;
|
|
101
|
+
try {
|
|
102
|
+
text = readFileSync(path, "utf8");
|
|
103
|
+
} catch (error) {
|
|
104
|
+
fail(`${label} を読み込めません: ${path}\n${error.message}`);
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(text);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
fail(`${label} が有効な JSON ではありません: ${path}\n${error.message}`);
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// analysis.schema.json の必須構造のみを検査する軽量チェック(zero-dep 方針のため ajv 等は
|
|
115
|
+
// 導入しない)。events[] の判別ユニオンの厳密な検証は行わない — 明らかな壊れ入力の拒否が目的。
|
|
116
|
+
function validateAnalysisStructure(analysis, label) {
|
|
117
|
+
const errors = [];
|
|
118
|
+
if (!isRecord(analysis)) {
|
|
119
|
+
errors.push("ルートは object である必要があります");
|
|
120
|
+
return errors;
|
|
121
|
+
}
|
|
122
|
+
if (analysis.version !== 0) errors.push("version は 0 である必要があります");
|
|
123
|
+
if (!isNonEmptyString(analysis.source)) errors.push("source は空でない文字列である必要があります");
|
|
124
|
+
for (const field of ["transcript", "keyframes", "events"]) {
|
|
125
|
+
if (!Array.isArray(analysis[field])) errors.push(`${field} は配列である必要があります`);
|
|
126
|
+
}
|
|
127
|
+
if (!isRecord(analysis.tracks)) {
|
|
128
|
+
errors.push("tracks は object である必要があります");
|
|
129
|
+
} else {
|
|
130
|
+
if (!Array.isArray(analysis.tracks.speakers)) errors.push("tracks.speakers は配列である必要があります");
|
|
131
|
+
if (!Array.isArray(analysis.tracks.faces)) errors.push("tracks.faces は配列である必要があります");
|
|
132
|
+
if (
|
|
133
|
+
!hasOwn(analysis.tracks, "person_matte") ||
|
|
134
|
+
(analysis.tracks.person_matte !== null && typeof analysis.tracks.person_matte !== "string")
|
|
135
|
+
) {
|
|
136
|
+
errors.push("tracks.person_matte は string か null である必要があります");
|
|
137
|
+
}
|
|
138
|
+
for (const field of ["face_landmarks", "hand_pose", "body_pose_3d", "face_expression"]) {
|
|
139
|
+
if (!hasOwn(analysis.tracks, field)) continue;
|
|
140
|
+
const pointer = analysis.tracks[field];
|
|
141
|
+
if (!isRecord(pointer) || !isNonEmptyString(pointer.path) || !(Number(pointer.sample_fps) > 0)) {
|
|
142
|
+
errors.push(`tracks.${field} は path(空でない文字列)・sample_fps(正数) を持つ object である必要があります`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (hasOwn(pointer, "features") && (
|
|
146
|
+
!Array.isArray(pointer.features)
|
|
147
|
+
|| pointer.features.some((feature) => !isNonEmptyString(feature))
|
|
148
|
+
|| new Set(pointer.features).size !== pointer.features.length
|
|
149
|
+
)) {
|
|
150
|
+
errors.push(`tracks.${field}.features は重複のない空でない文字列配列である必要があります`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const [index, kf] of (analysis.keyframes || []).entries()) {
|
|
155
|
+
if (!isRecord(kf)) {
|
|
156
|
+
errors.push(`keyframes[${index}] は object である必要があります`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (typeof kf.t !== "number") errors.push(`keyframes[${index}].t は数値である必要があります`);
|
|
160
|
+
if (!isNonEmptyString(kf.path)) errors.push(`keyframes[${index}].path は空でない文字列である必要があります`);
|
|
161
|
+
if (!isNonEmptyString(kf.note)) errors.push(`keyframes[${index}].note は空でない文字列である必要があります`);
|
|
162
|
+
}
|
|
163
|
+
for (const [index, seg] of (analysis.transcript || []).entries()) {
|
|
164
|
+
if (!isRecord(seg) || typeof seg.start !== "number" || typeof seg.end !== "number" || !isNonEmptyString(seg.text)) {
|
|
165
|
+
errors.push(`transcript[${index}] は start/end(数値)・text(空でない文字列) を持つ object である必要があります`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const [index, event] of (analysis.events || []).entries()) {
|
|
169
|
+
if (!isRecord(event) || !isNonEmptyString(event.type)) {
|
|
170
|
+
errors.push(`events[${index}] は type を持つ object である必要があります`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (event.type === "chapter" && typeof event.t !== "number") {
|
|
174
|
+
errors.push(`events[${index}] (chapter) は t(数値) が必要です`);
|
|
175
|
+
}
|
|
176
|
+
if (["trouble", "filler", "hook", "highlight"].includes(event.type)) {
|
|
177
|
+
if (typeof event.start !== "number" || typeof event.end !== "number") {
|
|
178
|
+
errors.push(`events[${index}] (${event.type}) は start/end(数値) が必要です`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return errors;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function hasOwn(object, key) {
|
|
186
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function toPosixRelative(fromDir, toPath) {
|
|
190
|
+
const rel = relative(fromDir, toPath);
|
|
191
|
+
return rel.split(sep).join("/");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// --- block-id 導出(doc:<path>#<block-id> 注釈ターゲットの地ならし)---
|
|
195
|
+
//
|
|
196
|
+
// 内部契約 contract-2026-07-26-doc-image-annotations.md §1 の 3 要件
|
|
197
|
+
// (データ由来・再生成安定・文書内一意)を満たす block-id を、埋め込む生データ
|
|
198
|
+
// (bundle)から導出する純関数群。生成した blocks マニフェストは bundle 本体には
|
|
199
|
+
// 混ぜず、別の <script type="application/json" id="akari-analysis-report-blocks">
|
|
200
|
+
// として template.html 側へ埋め込む(「生データをそのまま埋め込む」原則を保つため)。
|
|
201
|
+
|
|
202
|
+
class BlockIdCollisionError extends Error {
|
|
203
|
+
constructor(duplicateIds) {
|
|
204
|
+
super(`block-id が文書内で衝突しています: ${duplicateIds.join(", ")}`);
|
|
205
|
+
this.duplicateIds = duplicateIds;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// URL fragment として安全にするため、id を構成する各データ由来セグメントを
|
|
210
|
+
// percent-encode する(encodeURIComponent は "#" を含め fragment 中で意味を持つ
|
|
211
|
+
// 文字を必ず潰す)。セグメント間の区切りは常に ":"(encodeURIComponent は ":" を
|
|
212
|
+
// 必ずエンコードするため、区切りとセグメント内部の値が衝突しない)。
|
|
213
|
+
function blockId(kind, ...parts) {
|
|
214
|
+
return [kind, ...parts.map((part) => encodeURIComponent(String(part)))].join(":");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// analysis.events の chapter を開始秒昇順で並べたキー列。template.html の
|
|
218
|
+
// chaptersOf() と同じフィルタ・ソートを独立に再現している。章そのものには
|
|
219
|
+
// analysis.schema 上 id 相当のフィールドが無いため、開始秒 (t) を「データ由来
|
|
220
|
+
// キー(無ければ章開始時刻)」の「無ければ」側の安定キーとして採用する。
|
|
221
|
+
// chaptersOf() のソートロジックを変えるときはここも合わせて直すこと。
|
|
222
|
+
function chapterStartKeysOf(analysis) {
|
|
223
|
+
return (analysis.events || [])
|
|
224
|
+
.filter((event) => event.type === "chapter")
|
|
225
|
+
.map((event) => event.t)
|
|
226
|
+
.sort((a, b) => a - b);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// bundle(このスクリプトが template.html へ埋め込む生データそのもの)から
|
|
230
|
+
// blocks マニフェストを導出する。assets[] や relations[] / open_questions[] の
|
|
231
|
+
// 並び順には一切依存しない(ref・target/kind・question.id など内容由来のキーのみ
|
|
232
|
+
// 使う)ため、入力の並べ替えに対して安定する。
|
|
233
|
+
function buildBlocksManifest(bundle) {
|
|
234
|
+
const ids = [];
|
|
235
|
+
const record = (id) => {
|
|
236
|
+
ids.push(id);
|
|
237
|
+
return id;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const interpAssetsByRef = new Map(
|
|
241
|
+
(bundle.interpretation.assets || []).map((asset) => [asset.ref, asset]),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const byRef = {};
|
|
245
|
+
for (const asset of bundle.assets) {
|
|
246
|
+
const ref = asset.ref;
|
|
247
|
+
const analysis = asset.analysis;
|
|
248
|
+
const interpAsset = interpAssetsByRef.get(ref);
|
|
249
|
+
|
|
250
|
+
const chapterStartKeys = chapterStartKeysOf(analysis);
|
|
251
|
+
const chapters = {};
|
|
252
|
+
if (chapterStartKeys.length === 0) {
|
|
253
|
+
// 章情報が無い素材は transcript 全体を 1 ブロックとして扱う(template.html
|
|
254
|
+
// 側の「章情報なし」details 1 個に対応)。
|
|
255
|
+
chapters.unchaptered = record(blockId("transcript-chapter", ref, "unchaptered"));
|
|
256
|
+
} else {
|
|
257
|
+
for (const start of chapterStartKeys) {
|
|
258
|
+
chapters[String(start)] = record(blockId("transcript-chapter", ref, start));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const images = {};
|
|
263
|
+
for (const kf of analysis.keyframes || []) {
|
|
264
|
+
// 画像が実在確認できた(imageSrc が非 null)キーフレームのみ block-id を持つ
|
|
265
|
+
// — template.html は imageSrc が無いキーフレームには img 要素を描画しない。
|
|
266
|
+
if (kf.imageSrc) {
|
|
267
|
+
images[kf.path] = record(blockId("image", ref, kf.path));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const relations = {};
|
|
272
|
+
for (const relation of (interpAsset && interpAsset.relations) || []) {
|
|
273
|
+
const key = JSON.stringify([relation.target, relation.kind]);
|
|
274
|
+
relations[key] = record(blockId("relation", ref, relation.target, relation.kind));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
byRef[ref] = {
|
|
278
|
+
timeline: record(blockId("asset-timeline", ref)),
|
|
279
|
+
facts: record(blockId("asset-facts", ref)),
|
|
280
|
+
chapters,
|
|
281
|
+
images,
|
|
282
|
+
relations,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const questions = {};
|
|
287
|
+
for (const question of bundle.interpretation.open_questions || []) {
|
|
288
|
+
questions[question.id] = record(blockId("question", question.id));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 来歴(節 6)は「節単位で可」(契約 §1 対象ブロック表)に従い固定キー 1 個。
|
|
292
|
+
const provenance = record(blockId("provenance", "section"));
|
|
293
|
+
|
|
294
|
+
const counts = new Map();
|
|
295
|
+
for (const id of ids) counts.set(id, (counts.get(id) || 0) + 1);
|
|
296
|
+
const duplicates = [...counts.entries()]
|
|
297
|
+
.filter(([, count]) => count > 1)
|
|
298
|
+
.map(([id]) => id);
|
|
299
|
+
if (duplicates.length > 0) {
|
|
300
|
+
throw new BlockIdCollisionError(duplicates);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return { version: 0, byRef, questions, provenance };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// --- FK 結合(2026-07-22 A3.2 の swap 実証への対応)---
|
|
307
|
+
//
|
|
308
|
+
// 位置対応づけ(i 番目の --analysis が assets[i])を廃止し、ref で結合する。
|
|
309
|
+
// 正式形は --analysis <ref>=<path>。素の <path> のみの指定も許容するが、
|
|
310
|
+
// interpretation.json の inputs.analyses[].path と一意に照合できる場合に限る
|
|
311
|
+
// (basename または path segment の末尾一致が 1 件に定まる場合)。どちらの形でも、
|
|
312
|
+
// 最終的に確定した ref に対応する inputs.analyses[].path と、CLI で与えた path が
|
|
313
|
+
// 対応しているかを追加でクロスチェックする — ref=path 形式で意図的/誤って
|
|
314
|
+
// 取り違えたペアを渡した場合もここで検出する。
|
|
315
|
+
|
|
316
|
+
function pathSegments(value) {
|
|
317
|
+
return value
|
|
318
|
+
.split(/[\\/]+/)
|
|
319
|
+
.filter((segment) => segment.length > 0 && segment !== ".");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// b の path segment 列が a の末尾(または a が b の末尾)と一致するかを見る。
|
|
323
|
+
// 双方の basename(segment 数 1)同士の比較もこの関数でカバーされる。
|
|
324
|
+
function isPathSuffixMatch(a, b) {
|
|
325
|
+
const segA = pathSegments(a);
|
|
326
|
+
const segB = pathSegments(b);
|
|
327
|
+
if (segA.length === 0 || segB.length === 0) return false;
|
|
328
|
+
const [shorter, longer] = segA.length <= segB.length ? [segA, segB] : [segB, segA];
|
|
329
|
+
const offset = longer.length - shorter.length;
|
|
330
|
+
for (let i = 0; i < shorter.length; i += 1) {
|
|
331
|
+
if (longer[offset + i] !== shorter[i]) return false;
|
|
332
|
+
}
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function pathsCorrespond(rawPath, recordedPath, interpretationDir) {
|
|
337
|
+
if (isPathSuffixMatch(rawPath, recordedPath)) return true;
|
|
338
|
+
try {
|
|
339
|
+
return resolve(rawPath) === resolve(interpretationDir, recordedPath);
|
|
340
|
+
} catch {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 素の path 指定から、inputs.analyses[].path との一意照合で ref を確定する。
|
|
346
|
+
// 一致 0 件・複数件(曖昧)はどちらもハードエラー(順序へのフォールバックはしない)。
|
|
347
|
+
// pathsCorrespond と同じ判定(suffix match または絶対パス一致)を使う — 相対パスに
|
|
348
|
+
// ".." が含まれる場合、素朴な segment 末尾一致だけでは判定できないため。
|
|
349
|
+
function resolveBareAnalysisRef(rawPath, analysesEntries, interpretationDir) {
|
|
350
|
+
const matchingRefs = new Set();
|
|
351
|
+
for (const entry of analysesEntries) {
|
|
352
|
+
if (!entry || typeof entry.path !== "string" || typeof entry.ref !== "string") continue;
|
|
353
|
+
if (pathsCorrespond(rawPath, entry.path, interpretationDir)) matchingRefs.add(entry.ref);
|
|
354
|
+
}
|
|
355
|
+
if (matchingRefs.size === 0) {
|
|
356
|
+
fail(
|
|
357
|
+
`--analysis の path が inputs.analyses[].path のいずれとも一致しません` +
|
|
358
|
+
`(--analysis <ref>=<path> 形式で明示してください): ${rawPath}`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
if (matchingRefs.size > 1) {
|
|
362
|
+
fail(
|
|
363
|
+
`--analysis の path が inputs.analyses[].path に複数一致し一意に定まりません` +
|
|
364
|
+
`(--analysis <ref>=<path> 形式で明示してください): ${rawPath}`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
return [...matchingRefs][0];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// --analysis 引数群を解決し、Map<ref, rawPath>(CLI で与えられた生の path 文字列)を返す。
|
|
371
|
+
function resolveAnalysisArgs(analysisArgs, interpretation, interpretationDir) {
|
|
372
|
+
const interpAssets = interpretation.assets || [];
|
|
373
|
+
const assetRefs = new Set(interpAssets.map((asset) => asset.ref));
|
|
374
|
+
const analysesEntries = (interpretation.inputs && interpretation.inputs.analyses) || [];
|
|
375
|
+
const analysesEntryByRef = new Map(
|
|
376
|
+
analysesEntries
|
|
377
|
+
.filter((entry) => entry && typeof entry.ref === "string")
|
|
378
|
+
.map((entry) => [entry.ref, entry]),
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const pathByRef = new Map();
|
|
382
|
+
|
|
383
|
+
for (const analysisArg of analysisArgs) {
|
|
384
|
+
const eqIndex = analysisArg.indexOf("=");
|
|
385
|
+
let ref;
|
|
386
|
+
let rawPath;
|
|
387
|
+
if (eqIndex > 0) {
|
|
388
|
+
ref = analysisArg.slice(0, eqIndex);
|
|
389
|
+
rawPath = analysisArg.slice(eqIndex + 1);
|
|
390
|
+
if (!rawPath) {
|
|
391
|
+
fail(`--analysis の指定が不正です(ref=path の path が空です): ${analysisArg}`);
|
|
392
|
+
}
|
|
393
|
+
if (!assetRefs.has(ref)) {
|
|
394
|
+
fail(`--analysis の ref が interpretation.assets[].ref に存在しません: ${ref}(${analysisArg})`);
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
rawPath = analysisArg;
|
|
398
|
+
ref = resolveBareAnalysisRef(rawPath, analysesEntries, interpretationDir);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// FK クロスチェック: 確定した ref に対応する inputs.analyses[].path と、CLI で
|
|
402
|
+
// 与えられた path が対応しているかを検証する(取り違えの実証への対応)。
|
|
403
|
+
const recordedEntry = analysesEntryByRef.get(ref);
|
|
404
|
+
if (recordedEntry && !pathsCorrespond(rawPath, recordedEntry.path, interpretationDir)) {
|
|
405
|
+
fail(
|
|
406
|
+
`--analysis の path が ref『${ref}』の inputs.analyses[].path` +
|
|
407
|
+
`(${recordedEntry.path})と対応していません: ${rawPath}` +
|
|
408
|
+
"(取り違えの疑いがあります。--analysis <ref>=<path> の対応を再確認してください)",
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (pathByRef.has(ref)) {
|
|
413
|
+
fail(`--analysis に同じ ref が重複して指定されています: ${ref}`);
|
|
414
|
+
}
|
|
415
|
+
pathByRef.set(ref, rawPath);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const missingRefs = [...assetRefs].filter((ref) => !pathByRef.has(ref));
|
|
419
|
+
if (missingRefs.length > 0) {
|
|
420
|
+
fail(`assets[].ref に対応する --analysis が指定されていません: ${missingRefs.join(", ")}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return pathByRef;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function main() {
|
|
427
|
+
let args;
|
|
428
|
+
try {
|
|
429
|
+
args = parseArgs(process.argv.slice(2));
|
|
430
|
+
} catch (error) {
|
|
431
|
+
console.error(error.message);
|
|
432
|
+
console.error("");
|
|
433
|
+
console.error(usage());
|
|
434
|
+
process.exit(2);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (args.help) {
|
|
438
|
+
console.log(usage());
|
|
439
|
+
process.exit(0);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const interpretationAbsolutePath = resolve(args.interpretationPath);
|
|
443
|
+
if (!existsSync(interpretationAbsolutePath)) {
|
|
444
|
+
fail(`interpretation.json が見つかりません: ${interpretationAbsolutePath}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// interpretation.json は SSOT である packages/schemas/bin/validate-interpretation.mjs で
|
|
448
|
+
// 検証する(このスクリプト側でロジックを複製しない)。PASS しなければ何も書き出さず終了する。
|
|
449
|
+
try {
|
|
450
|
+
execFileSync(process.execPath, [validateInterpretationBin, interpretationAbsolutePath], {
|
|
451
|
+
stdio: "pipe",
|
|
452
|
+
});
|
|
453
|
+
} catch (error) {
|
|
454
|
+
console.error(`interpretation.json の検証に失敗しました(validate-interpretation.mjs): ${interpretationAbsolutePath}`);
|
|
455
|
+
if (error.stdout) console.error(error.stdout.toString());
|
|
456
|
+
if (error.stderr) console.error(error.stderr.toString());
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const interpretation = readJson(interpretationAbsolutePath, "interpretation.json");
|
|
461
|
+
const interpAssets = interpretation.assets || [];
|
|
462
|
+
const interpretationDir = dirname(interpretationAbsolutePath);
|
|
463
|
+
|
|
464
|
+
const pathByRef = resolveAnalysisArgs(args.analysisPaths, interpretation, interpretationDir);
|
|
465
|
+
|
|
466
|
+
const outAbsolutePath = resolve(args.outPath);
|
|
467
|
+
const outDir = dirname(outAbsolutePath);
|
|
468
|
+
|
|
469
|
+
// assets[] は常に interpretation.assets[] の順序で束ねる(CLI 引数の順序には依存しない)。
|
|
470
|
+
const assets = [];
|
|
471
|
+
for (const interpAsset of interpAssets) {
|
|
472
|
+
const analysisArg = pathByRef.get(interpAsset.ref);
|
|
473
|
+
const analysisAbsolutePath = resolve(analysisArg);
|
|
474
|
+
if (!existsSync(analysisAbsolutePath)) {
|
|
475
|
+
fail(`analysis.json が見つかりません: ${analysisAbsolutePath}`);
|
|
476
|
+
}
|
|
477
|
+
const analysis = readJson(analysisAbsolutePath, "analysis.json");
|
|
478
|
+
const errors = validateAnalysisStructure(analysis, analysisAbsolutePath);
|
|
479
|
+
if (errors.length > 0) {
|
|
480
|
+
console.error(`analysis.json の構造検証に失敗しました: ${analysisAbsolutePath}`);
|
|
481
|
+
for (const message of errors) console.error(`- ${message}`);
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const analysisDir = dirname(analysisAbsolutePath);
|
|
486
|
+
const resolvedKeyframes = (analysis.keyframes || []).map((kf) => {
|
|
487
|
+
const kfAbsolutePath = resolve(analysisDir, kf.path);
|
|
488
|
+
let imageSrc = null;
|
|
489
|
+
try {
|
|
490
|
+
if (existsSync(kfAbsolutePath) && statSync(kfAbsolutePath).isFile()) {
|
|
491
|
+
imageSrc = toPosixRelative(outDir, kfAbsolutePath);
|
|
492
|
+
}
|
|
493
|
+
} catch {
|
|
494
|
+
imageSrc = null;
|
|
495
|
+
}
|
|
496
|
+
return { ...kf, imageSrc };
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
assets.push({
|
|
500
|
+
ref: interpAsset.ref,
|
|
501
|
+
analysisPath: isAbsolute(analysisArg) ? analysisArg : toPosixRelative(process.cwd(), analysisAbsolutePath),
|
|
502
|
+
analysis: { ...analysis, keyframes: resolvedKeyframes },
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const bundle = {
|
|
507
|
+
version: 0,
|
|
508
|
+
generatedAt: new Date().toISOString(),
|
|
509
|
+
assets,
|
|
510
|
+
interpretation,
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
let blocksManifest;
|
|
514
|
+
try {
|
|
515
|
+
blocksManifest = buildBlocksManifest(bundle);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
if (error instanceof BlockIdCollisionError) {
|
|
518
|
+
fail(`block-id の導出に失敗しました: ${error.message}`);
|
|
519
|
+
}
|
|
520
|
+
throw error;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const serialized = JSON.stringify(bundle).replace(/</g, "\\u003c");
|
|
524
|
+
const serializedBlocks = JSON.stringify(blocksManifest).replace(/</g, "\\u003c");
|
|
525
|
+
|
|
526
|
+
const templateText = readFileSync(templatePath, "utf8");
|
|
527
|
+
if (!templateText.includes(DATA_PLACEHOLDER)) {
|
|
528
|
+
fail(`template.html の埋め込み用プレースホルダーが見つかりません: ${templatePath}`);
|
|
529
|
+
}
|
|
530
|
+
if (!templateText.includes(BLOCKS_PLACEHOLDER)) {
|
|
531
|
+
fail(`template.html の block-id マニフェスト埋め込み用プレースホルダーが見つかりません: ${templatePath}`);
|
|
532
|
+
}
|
|
533
|
+
const rendered = templateText
|
|
534
|
+
.replace(DATA_PLACEHOLDER, serialized)
|
|
535
|
+
.replace(BLOCKS_PLACEHOLDER, serializedBlocks);
|
|
536
|
+
|
|
537
|
+
mkdirSync(outDir, { recursive: true });
|
|
538
|
+
writeFileSync(outAbsolutePath, rendered, "utf8");
|
|
539
|
+
|
|
540
|
+
const bytes = Buffer.byteLength(rendered, "utf8");
|
|
541
|
+
console.log(`OK: ${outAbsolutePath} (${bytes.toLocaleString("en-US")} bytes)`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
main();
|