akari-video 1.1.1 → 1.2.0-beta.2
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/clean-manifest.mjs +1 -0
- package/src/doctor-command.mjs +7 -2
- package/src/migrate-command.mjs +8 -3
- package/vendor/.akari-capability-sources.json +6 -0
- package/vendor/docs/contract-2026-07-13-asset-library.md +1 -1
- package/vendor/docs/contract-2026-07-25-project-structure-v0.md +3 -0
- package/vendor/docs/contract-2026-08-30-edit-json-v2-object-tree-v0.md +16 -2
- package/vendor/docs/contract-2026-08-30-motion-and-keyframes-v0.md +1 -1
- package/vendor/docs/contract-2026-10-07-material-ranges-v0.md +13 -0
- package/vendor/docs/contract-2026-10-07-tasks-v0.md +74 -0
- package/vendor/packages/akari-ear/README.md +9 -0
- package/vendor/packages/akari-ear/package.json +11 -0
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/akari-vibe/README.md +2 -2
- package/vendor/packages/asset-resolver/src/catalog.mjs +21 -4
- package/vendor/packages/asset-resolver/src/project-references.mjs +5 -0
- package/vendor/packages/asset-resolver/src/resolve.mjs +103 -1
- package/vendor/packages/asset-resolver/src/state.mjs +1 -1
- package/vendor/packages/asset-resolver/test/catalog-and-state.test.mjs +26 -1
- package/vendor/packages/asset-resolver/test/fixtures/fragment-root-timing.json +35 -0
- package/vendor/packages/asset-resolver/test/fragment-project-copy.test.mjs +45 -0
- package/vendor/packages/asset-resolver/test/project-references.test.mjs +6 -1
- package/vendor/packages/asset-resolver/test/resolve-fast.test.mjs +12 -0
- package/vendor/packages/edit-lint/src/engine-capabilities.json +2 -0
- package/vendor/packages/edit-lint/src/lint/captions.mjs +9 -0
- package/vendor/packages/edit-lint/src/lint/edit-v2.mjs +30 -0
- package/vendor/packages/edit-lint/src/lint/overlays.mjs +4 -26
- package/vendor/packages/edit-store/lib/canonical.js +12 -2
- package/vendor/packages/edit-store/lib/caption-display.js +4 -2
- package/vendor/packages/edit-store/lib/caption-timeline.d.ts +1 -0
- package/vendor/packages/edit-store/lib/caption-timeline.js +43 -23
- package/vendor/packages/edit-store/lib/cut-ranges.js +500 -31
- package/vendor/packages/edit-store/lib/edit-v2.d.ts +11 -0
- package/vendor/packages/edit-store/lib/edit-v2.js +33 -3
- package/vendor/packages/edit-store/lib/empty-edit-v2.d.ts +8 -0
- package/vendor/packages/edit-store/lib/empty-edit-v2.js +17 -0
- package/vendor/packages/edit-store/lib/generated/edit-v2-keys.d.ts +4 -4
- package/vendor/packages/edit-store/lib/generated/edit-v2-keys.js +4 -2
- 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.d.ts +3 -0
- package/vendor/packages/edit-store/lib/internal-model.js +3 -49
- package/vendor/packages/edit-store/lib/migrate/index.d.ts +2 -0
- package/vendor/packages/edit-store/lib/migrate/index.js +14 -1
- package/vendor/packages/edit-store/lib/tasks-store.d.ts +36 -0
- package/vendor/packages/edit-store/lib/tasks-store.js +273 -0
- package/vendor/packages/edit-store/lib/webview-kernel.js +75 -58
- package/vendor/packages/frame-scene/README.md +62 -0
- package/vendor/packages/frame-scene/package.json +9 -0
- package/vendor/packages/overlay-runtime/src/fragment-source-write.mjs +16 -1
- package/vendor/packages/overlay-runtime/src/handle-geometry.js +50 -5
- package/vendor/packages/overlay-runtime/src/interaction.js +217 -151
- package/vendor/packages/overlay-runtime/src/parts.d.ts +6 -0
- package/vendor/packages/overlay-runtime/src/parts.mjs +75 -3
- package/vendor/packages/project-scaffold/src/index.mjs +2 -6
- package/vendor/packages/render-cut/README.ja.md +2 -0
- package/vendor/packages/render-cut/README.md +2 -0
- package/vendor/packages/schemas/asset-meta.schema.json +25 -0
- package/vendor/packages/schemas/bin/validate-browser-engines.mjs +57 -0
- package/vendor/packages/schemas/bin/validate-captions.mjs +6 -0
- package/vendor/packages/schemas/bin/validate-frame-scene.mjs +24 -0
- package/vendor/packages/schemas/bin/validate-tasks.mjs +100 -0
- package/vendor/packages/schemas/bin/validate-voice-dictionary.mjs +81 -0
- package/vendor/packages/schemas/browser-engines.schema.json +30 -0
- package/vendor/packages/schemas/edit.schema.json +19 -0
- package/vendor/packages/schemas/engine-capabilities.json +2 -0
- package/vendor/packages/schemas/examples/voice-dictionary-v0-invalid-jev-scope/voice-dictionary.json +3 -0
- package/vendor/packages/schemas/examples/voice-dictionary-v0-invalid-key-conflict/voice-dictionary.json +4 -0
- package/vendor/packages/schemas/examples/voice-dictionary-v0-invalid-version-1/voice-dictionary.json +1 -0
- package/vendor/packages/schemas/examples/voice-dictionary-v0-valid/voice-dictionary.json +4 -0
- package/vendor/packages/schemas/fixtures/asset-meta-frame-scene/model-invalid-unit.json +129 -0
- package/vendor/packages/schemas/fixtures/asset-meta-frame-scene/model-valid.json +129 -0
- package/vendor/packages/schemas/fixtures/asset-meta-frame-scene/world-invalid-kind.json +114 -0
- package/vendor/packages/schemas/fixtures/asset-meta-frame-scene/world-valid.json +114 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/anchor.unresolved/expected.json +6 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/anchor.unresolved/scene.json +37 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/limit.objects/expected.json +6 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/limit.objects/scene.json +2029 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/object.id.duplicate/expected.json +6 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/object.id.duplicate/scene.json +37 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/object.rotation.exclusive/expected.json +6 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/object.rotation.exclusive/scene.json +30 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/object.size.exclusive/expected.json +6 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/object.size.exclusive/scene.json +30 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/on.unresolved/expected.json +6 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/on.unresolved/scene.json +29 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/pose.joint.unknown/expected.json +6 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/pose.joint.unknown/scene.json +35 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/pose.kind.invalid/expected.json +6 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/pose.kind.invalid/scene.json +34 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/scene.empty/expected.json +6 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/scene.empty/scene.json +16 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/unsupported_version/expected.json +6 -0
- package/vendor/packages/schemas/fixtures/frame-scene/invalid/unsupported_version/scene.json +28 -0
- package/vendor/packages/schemas/fixtures/frame-scene/valid/at-limits/expected.json +4 -0
- package/vendor/packages/schemas/fixtures/frame-scene/valid/at-limits/scene.json +1 -0
- package/vendor/packages/schemas/fixtures/frame-scene/valid/camera-a-b/expected.json +4 -0
- package/vendor/packages/schemas/fixtures/frame-scene/valid/camera-a-b/scene.json +36 -0
- package/vendor/packages/schemas/fixtures/frame-scene/valid/empty-stage-scribble/expected.json +4 -0
- package/vendor/packages/schemas/fixtures/frame-scene/valid/empty-stage-scribble/scene.json +30 -0
- package/vendor/packages/schemas/fixtures/frame-scene/valid/hdri-hidden/expected.json +4 -0
- package/vendor/packages/schemas/fixtures/frame-scene/valid/hdri-hidden/scene.json +33 -0
- package/vendor/packages/schemas/fixtures/frame-scene/valid/minimal/expected.json +4 -0
- package/vendor/packages/schemas/fixtures/frame-scene/valid/minimal/scene.json +28 -0
- package/vendor/packages/schemas/fixtures/frame-scene/valid/multi-annotation-anchor/expected.json +4 -0
- package/vendor/packages/schemas/fixtures/frame-scene/valid/multi-annotation-anchor/scene.json +70 -0
- package/vendor/packages/schemas/fixtures/frame-scene/valid/multi-scene-set/expected.json +4 -0
- package/vendor/packages/schemas/fixtures/frame-scene/valid/multi-scene-set/scene.json +40 -0
- package/vendor/packages/schemas/fixtures/frame-scene/valid/pose-targets/expected.json +4 -0
- package/vendor/packages/schemas/fixtures/frame-scene/valid/pose-targets/scene.json +40 -0
- package/vendor/packages/schemas/fixtures/generation-meta/frame-invalid-hash.json +84 -0
- package/vendor/packages/schemas/fixtures/generation-meta/frame-invalid-kind.json +84 -0
- package/vendor/packages/schemas/fixtures/generation-meta/frame-invalid-role.json +84 -0
- package/vendor/packages/schemas/fixtures/generation-meta/frame-valid.json +84 -0
- package/vendor/packages/schemas/frame-scene.schema.json +99 -0
- package/vendor/packages/schemas/generation-meta.schema.json +39 -0
- package/vendor/packages/schemas/package.json +5 -2
- package/vendor/packages/schemas/tasks.schema.json +115 -0
- package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +15 -0
- package/vendor/packages/schemas/test/element-overrides.test.mjs +17 -0
- package/vendor/packages/schemas/test/engine-capabilities.test.mjs +1 -1
- package/vendor/packages/schemas/test/fixtures/tasks/invalid/duplicate-id/expected.json +3 -0
- package/vendor/packages/schemas/test/fixtures/tasks/invalid/duplicate-id/tasks.json +17 -0
- package/vendor/packages/schemas/test/fixtures/tasks/invalid/invalid-id/expected.json +3 -0
- package/vendor/packages/schemas/test/fixtures/tasks/invalid/invalid-id/tasks.json +11 -0
- package/vendor/packages/schemas/test/fixtures/tasks/invalid/missing-source/expected.json +3 -0
- package/vendor/packages/schemas/test/fixtures/tasks/invalid/missing-source/tasks.json +10 -0
- package/vendor/packages/schemas/test/fixtures/tasks/invalid/missing-state/expected.json +3 -0
- package/vendor/packages/schemas/test/fixtures/tasks/invalid/missing-state/tasks.json +10 -0
- package/vendor/packages/schemas/test/fixtures/tasks/invalid/newer-version/expected.json +3 -0
- package/vendor/packages/schemas/test/fixtures/tasks/invalid/newer-version/tasks.json +4 -0
- package/vendor/packages/schemas/test/fixtures/tasks/invalid/outbound-auto/expected.json +3 -0
- package/vendor/packages/schemas/test/fixtures/tasks/invalid/outbound-auto/tasks.json +13 -0
- package/vendor/packages/schemas/test/fixtures/tasks/invalid/tasks-not-array/expected.json +3 -0
- package/vendor/packages/schemas/test/fixtures/tasks/invalid/tasks-not-array/tasks.json +4 -0
- package/vendor/packages/schemas/test/fixtures/tasks/invalid/top-array/expected.json +3 -0
- package/vendor/packages/schemas/test/fixtures/tasks/invalid/top-array/tasks.json +1 -0
- package/vendor/packages/schemas/test/fixtures/tasks/valid/all-fields/expected.json +3 -0
- package/vendor/packages/schemas/test/fixtures/tasks/valid/all-fields/tasks.json +92 -0
- package/vendor/packages/schemas/test/fixtures/tasks/valid/annotation-overlay/expected.json +3 -0
- package/vendor/packages/schemas/test/fixtures/tasks/valid/annotation-overlay/tasks.json +16 -0
- package/vendor/packages/schemas/test/fixtures/tasks/valid/e5/expected.json +3 -0
- package/vendor/packages/schemas/test/fixtures/tasks/valid/e5/tasks.json +43 -0
- package/vendor/packages/schemas/test/fixtures/tasks/valid/export/expected.json +3 -0
- package/vendor/packages/schemas/test/fixtures/tasks/valid/export/tasks.json +15 -0
- package/vendor/packages/schemas/test/fixtures/tasks/valid/lint/expected.json +3 -0
- package/vendor/packages/schemas/test/fixtures/tasks/valid/lint/tasks.json +15 -0
- package/vendor/packages/schemas/test/fixtures/tasks/valid/minimal/expected.json +3 -0
- package/vendor/packages/schemas/test/fixtures/tasks/valid/minimal/tasks.json +11 -0
- package/vendor/packages/schemas/test/fixtures/tasks/valid/unknown-fields/expected.json +3 -0
- package/vendor/packages/schemas/test/fixtures/tasks/valid/unknown-fields/tasks.json +18 -0
- package/vendor/packages/schemas/test/frame-scene-fixtures.test.mjs +18 -0
- package/vendor/packages/schemas/test/meta-frame-model3d-compat.test.mjs +25 -0
- package/vendor/packages/schemas/test/tasks-fixtures.test.mjs +42 -0
- package/vendor/packages/schemas/test/validate-browser-engines.test.mjs +18 -0
- package/vendor/packages/schemas/test/validate-captions.test.mjs +14 -0
- package/vendor/packages/schemas/test/validate-voice-dictionary.test.mjs +37 -0
- package/vendor/packages/schemas/voice-dictionary.schema.json +39 -0
- package/vendor/packages/word-book/src/index.mjs +17 -0
- package/vendor/skills/overlay-authoring/SKILL.md +18 -1
- package/vendor/skills/overlay-authoring/table.md +17 -0
- package/vendor/skills/overlay-authoring/telop.md +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0-beta.2",
|
|
4
4
|
"description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/clean-manifest.mjs
CHANGED
|
@@ -200,6 +200,7 @@ async function classifyAkari(akari, context) {
|
|
|
200
200
|
for (const candidate of child.children) addDisposable(candidate, rule('.akari/render-tmp/*'), context);
|
|
201
201
|
} else if (child.name === 'cache' && child.kind === 'directory') {
|
|
202
202
|
for (const candidate of child.children) addDisposable(candidate, rule('.akari/cache/**'), context);
|
|
203
|
+
} else if (child.name === 'tasks.json.lock' && child.kind === 'directory') { addDisposable(child, manifest('.akari/tasks.json.lock', 'disposable', '書き込み中の一時ロック', 'AKARI Video'), context);
|
|
203
204
|
} else if (child.name === 'diffs' && child.kind === 'directory') {
|
|
204
205
|
for (const candidate of child.children) addDisposable(candidate, rule('.akari/diffs/*'), context);
|
|
205
206
|
} else if (child.name === 'work' && child.kind === 'directory') {
|
package/src/doctor-command.mjs
CHANGED
|
@@ -60,9 +60,14 @@ export function formatDoctorReport(report) {
|
|
|
60
60
|
'',
|
|
61
61
|
`判定: ${report.verdict}`,
|
|
62
62
|
];
|
|
63
|
-
|
|
63
|
+
const steps = [...report.next_steps];
|
|
64
|
+
if (report.cli.node?.runtime === 'electron' && report.cli.node.exec_path && report.render_cut.path) {
|
|
65
|
+
const quote = (value) => `"${value.replaceAll('"', '\\"')}"`;
|
|
66
|
+
steps.push(`Node が PATH に無い場合の書き出し: ELECTRON_RUN_AS_NODE=1 ${quote(report.cli.node.exec_path)} ${quote(report.render_cut.path)} <project-root>`);
|
|
67
|
+
}
|
|
68
|
+
if (steps.length > 0) {
|
|
64
69
|
lines.push('次の手順:');
|
|
65
|
-
lines.push(...
|
|
70
|
+
lines.push(...steps.map((step) => ` - ${step}`));
|
|
66
71
|
}
|
|
67
72
|
return lines.join('\n');
|
|
68
73
|
}
|
package/src/migrate-command.mjs
CHANGED
|
@@ -64,11 +64,14 @@ export async function runMigrateCommand(args, options = {}) {
|
|
|
64
64
|
}
|
|
65
65
|
if (parsed.json) {
|
|
66
66
|
log(JSON.stringify({
|
|
67
|
-
ok: true, dryRun: parsed.dryRun, version: proposal.version,
|
|
67
|
+
ok: true, dryRun: parsed.dryRun, version: proposal.emptyProject ? null : proposal.version,
|
|
68
|
+
...(proposal.emptyProject ? { emptyProject: true } : {}),
|
|
68
69
|
filePath: proposal.filePath, backupPath: proposal.backupPath, changes: proposal.changes
|
|
69
70
|
}));
|
|
70
71
|
} else {
|
|
71
|
-
log(
|
|
72
|
+
log(proposal.emptyProject
|
|
73
|
+
? `初期化対象: ${proposal.filePath} (空の edit.json -> version 2)`
|
|
74
|
+
: `変換対象: ${proposal.filePath} (version ${proposal.version}${proposal.version === 2 ? ' 正規化' : ' -> 2'})`);
|
|
72
75
|
for (const change of proposal.changes) log(`- ${change.path}: ${change.note}`);
|
|
73
76
|
log(`変換前の退避先: ${proposal.backupPath}`);
|
|
74
77
|
}
|
|
@@ -92,7 +95,9 @@ export async function runMigrateCommand(args, options = {}) {
|
|
|
92
95
|
}
|
|
93
96
|
await migrate.applyMigration(proposal);
|
|
94
97
|
if (!parsed.json) {
|
|
95
|
-
log(proposal.
|
|
98
|
+
log(proposal.emptyProject
|
|
99
|
+
? `空の edit.json を初期化しました。元ファイル: ${proposal.backupPath}`
|
|
100
|
+
: proposal.version === 2
|
|
96
101
|
? `version 2 を正規化しました。元ファイル: ${proposal.backupPath}`
|
|
97
102
|
: `version 2 へ変換しました。元ファイル: ${proposal.backupPath}`);
|
|
98
103
|
}
|
|
@@ -83,6 +83,10 @@
|
|
|
83
83
|
"docs/contract-2026-10-02-textstyle-v1-rich.md",
|
|
84
84
|
"docs/contract-2026-10-04-asset-tier-v1.md",
|
|
85
85
|
"docs/contract-2026-10-06-timeline-ripple-v0.md",
|
|
86
|
+
"docs/contract-2026-10-07-material-ranges-v0.md",
|
|
87
|
+
"docs/contract-2026-10-07-tasks-v0.md",
|
|
88
|
+
"packages/akari-ear/package.json",
|
|
89
|
+
"packages/akari-ear/README.md",
|
|
86
90
|
"packages/akari-launcher/package.json",
|
|
87
91
|
"packages/akari-launcher/README.md",
|
|
88
92
|
"packages/akari-tools/package.json",
|
|
@@ -109,6 +113,8 @@
|
|
|
109
113
|
"packages/frame-engine/package.json",
|
|
110
114
|
"packages/frame-engine/README.ja.md",
|
|
111
115
|
"packages/frame-engine/README.md",
|
|
116
|
+
"packages/frame-scene/package.json",
|
|
117
|
+
"packages/frame-scene/README.md",
|
|
112
118
|
"packages/generate/package.json",
|
|
113
119
|
"packages/generate/README.md",
|
|
114
120
|
"packages/gpu-export/package.json",
|
|
@@ -373,7 +373,7 @@ catalog に載せる素材は、取得元のライセンスが CC0 相当(帰
|
|
|
373
373
|
|
|
374
374
|
| v1 category | 形の定義 | 判定 | 旧 category |
|
|
375
375
|
|---|---|---|---|
|
|
376
|
-
| `overlay` | 時間を持つ HTML 断片 | `fragment.html`
|
|
376
|
+
| `overlay` | 時間を持つ HTML 断片 | `fragment.html` を `overlays[]` から合成。断片ルートに時刻を置かず、素材本来の長さは `data-akari-natural-duration`(秒)に記録 | `telop` / `motion` |
|
|
377
377
|
| `still` | 時間を持たない HTML シート | `fragment.html`。決定的スクショで画像に焼く | `thumbnail` |
|
|
378
378
|
| `scene3d` | 3D モデル + 表示断片、またはベイクレシピ | `fragment.html` + glTF、または `scene.py` | `3d` |
|
|
379
379
|
| `audio` | 音声トラックに載るバイナリ | `edit.json` の `audio.bgm` / `sfx` が参照 | `audio`(変更なし) |
|
|
@@ -40,6 +40,7 @@ updated: 2026-09-06
|
|
|
40
40
|
|---|---|---|
|
|
41
41
|
| 人間の一次素材 | `assets/`(下位分類自由) | 人間・取り込み UI |
|
|
42
42
|
| 企画・プラン | `planning/` | スキル + 人間承認 |
|
|
43
|
+
| 仮枠のシーン JSON(AI が書く・人が微調整。仮枠の meta の `frame.content.scene` が指す) | `planning/frames/` | スキル・仮枠の編集 |
|
|
43
44
|
| エージェント中間物・ad-hoc 検証スクリプトと結果 | `.akari/work/` | スキル・エージェント |
|
|
44
45
|
| 検証証跡・レポート(キーフレーム視認 PNG・render-report 等) | `.akari/reports/` | スキル |
|
|
45
46
|
| 絵コンテ(タイムラインの印刷・初回だけ・AI は後から読まない) | `.akari/reports/storyboard/` | スキル |
|
|
@@ -47,6 +48,8 @@ updated: 2026-09-06
|
|
|
47
48
|
| 納品物 | `exports/` | `render-cut` |
|
|
48
49
|
| 契約サイドカー(既存) | `.akari/`(`sidecars/` `diffs/` `events/` 等) | 既存のまま |
|
|
49
50
|
|
|
51
|
+
`planning/frames/<stem>.scene.json` は仮枠(生成 v0 契約)の中身を記述する正本で、変更履歴に入る(`planning/**`)。仮枠の絵(png)と meta は従来どおり `assets/generated/`。
|
|
52
|
+
|
|
50
53
|
`.akari/` 直下にはこの表にない契約サイドカーファイルも存在する(`intake.json`・
|
|
51
54
|
`workflow.json`・`connections.json`・`lint.json`・`render.json` 等)。これらは各自の
|
|
52
55
|
契約文書が正本であり、本契約は「置き場所の層」を追加で確定させるだけで、個々のファイル形は
|
|
@@ -31,7 +31,7 @@ updated: 2026-08-30
|
|
|
31
31
|
|
|
32
32
|
## 1. データ模型(`version: 2` のまま追加のみ)
|
|
33
33
|
|
|
34
|
-
**新しいサブスキーマはここに列挙した以外増やさない。**
|
|
34
|
+
**新しいサブスキーマはここに列挙した以外増やさない。** HTML source の `elements` は要素ごとの inline style 上書きである。部品も字幕行もグループも「普通のアイテム」で、共通フィールド(`id` / `at` / `duration` / `transform` / `opacity` / `blend` / `crop` / `perspective` / `keyframes`)をそのまま使う。
|
|
35
35
|
|
|
36
36
|
### 1.1 再帰 — アイテムは `items[]` を持てる
|
|
37
37
|
|
|
@@ -81,7 +81,7 @@ updated: 2026-08-30
|
|
|
81
81
|
- **見える子 = 袋の名札から写す(projection)**。edit.json には書かない。上の例で `s01.html` に部品 A / B / C があれば、A は写し(触っていない・既定の時間 = 袋と同じ)、B は明示(触ったので `items[]` に居る)、C は `exclude`(袋の中では表示しない = 別の場所へ「出した」か、消した)
|
|
82
82
|
- **触った子だけ明示アイテムになる**。明示アイテムは袋の子として `items[]` に置くか、木の別の場所に置く(= 分離 §3.1)。どちらでも袋側は `source.exclude` に id を持つ
|
|
83
83
|
- **袋の中では並びを変えない・ばらさない**(HTML の DOM 順 / captions.json の行順が正)。変えたい部品は出す
|
|
84
|
-
- 名札の無い断片は「袋ごと 1 アイテム」(`part` 無し・`items`
|
|
84
|
+
- 名札の無い断片は「袋ごと 1 アイテム」(`part` 無し・`items` 無し)。このアイテムも `source.elements` による要素の上書きを持てる。既存の overlays はこれ(**回帰なし**)
|
|
85
85
|
- 名札の読み取り(HTML の走査)は**描画・プレビュー側の仕事**(§4)。edit.json の読み込み層は袋を「子を持ち得るアイテム」として扱うだけ
|
|
86
86
|
|
|
87
87
|
### 1.4 部品アイテム `source: { kind: "html", path, part }`
|
|
@@ -90,6 +90,14 @@ updated: 2026-08-30
|
|
|
90
90
|
- `vars` / `params`(既存のツマミ経路)は部品アイテムでも使える。`style` / `text` は「設計済みのツマミが無い所を直す」ための逃げ道で、これで HTML を書き換えずに見た目の自由度を持つ
|
|
91
91
|
- 部品アイテムの `at` / `duration` / `transform` / `opacity` / `keyframes` は共通フィールド。**部品専用のサブスキーマは無い**
|
|
92
92
|
|
|
93
|
+
### 1.4a 要素の上書き `source.elements`
|
|
94
|
+
|
|
95
|
+
- HTML source は任意の `elements: { "<番地>": { "style": { "<css-prop>": "<value>" } } }` を持てる。CSS 値は検証しない。番地の値に許すキーは `style` だけ。空の `elements` と空の `style` は保存時に畳んで消す。
|
|
96
|
+
- 番地は `#id[0]` または `.class[2]` の形。書式は `^[#.][^\s\[\]]+\[(0|[1-9]\d*)\]$`。id 属性の完全一致、または class 属性の空白区切りトークンの一致を、断片ソースの文書順に 0 から数える。CSS セレクタとして解釈しない。コメントと script/style の中身、`<script>` / `<style>` 要素そのもの(番地で指せない)、マウント後に増える要素は数えない。
|
|
97
|
+
- 開始タグを親子とも文書順に数え、重複 id も出現番号で区別する。属性名は大小文字を区別せず、class の区切りはタブ・改行を含む空白とする(`tokenizeHtml` と同じ)。
|
|
98
|
+
- 解決できない番地は描画で読み飛ばし、lint の `v2.element-ref` warning にする。素材 HTML は編集しない。
|
|
99
|
+
- 袋の `elements` は各子へ継承する。同じ番地の同じ CSS プロパティは子の値が勝つ。部品ルートに `source.style` もあるときは `elements` の後に `style` を追記し、部品の値が勝つ。
|
|
100
|
+
|
|
93
101
|
### 1.5 字幕 = 袋グループ(専用トラックの廃止)
|
|
94
102
|
|
|
95
103
|
```jsonc
|
|
@@ -198,6 +206,7 @@ updated: 2026-08-30
|
|
|
198
206
|
## 4. 描画 — クローンマスク
|
|
199
207
|
|
|
200
208
|
- **部品アイテム 1 つにつき断片全体を 1 回マウント**し、当該 `part` 以外の名札付き要素を `visibility: hidden` にする(`display: none` ではない — レイアウトと CSS の継承を壊さない)。各マウントが自分の時計(アイテム相対時間)・変形・z を持つ
|
|
209
|
+
- `source.elements` は要素を複製せず、同じマウント内の開始タグへ inline style を追記する。部品の複製がある場合は各複製へマスクより先に同じ上書きを適用する。`elements` が無い既存アイテムのレコードは変えない。
|
|
201
210
|
- 袋グループの写しの子(触っていない部品)は、時間・位置が袋と揃っている限り**1 マウントにまとめる**(エンジン側の最適化。データは不変)。`exclude` の部品は `visibility: hidden`
|
|
202
211
|
- `source.style` は部品ルート要素の inline style として適用、`source.text` は部品ルートの textContent を差し替える(子要素を持つ部品に `text` を指定したら lint warning・描画は最初のテキストノードだけ差し替え)
|
|
203
212
|
- 部品内の CSS / GSAP アニメ(L3)は部品アイテムの時計で seek する(既存の HTML seek 規約のまま)
|
|
@@ -297,6 +306,7 @@ await p.save(); // 正規直列化 → lint ゲート
|
|
|
297
306
|
| `v2.track-no-overlap` | error | 同一段の items が時間で重ならない(既存 → 明文化)|
|
|
298
307
|
| `v2.group-bake-blocked` | error | `keyframes` / `motion` / `animator` を持つグループを ungroup しようとした(edit-store の操作時)|
|
|
299
308
|
| `v2.part-ref` | warning | `source.part` / `source.exclude` の id が袋に存在するか(**文字列レベル**: HTML は `data-akari-part="…"` の grep、captions は行 id)|
|
|
309
|
+
| `v2.element-ref` | warning | `source.elements` の番地が `source.path` の HTML ソース内で解決できるか |
|
|
300
310
|
| `v2.captions-content-deprecated` | warning | 旧形 `tracks[].content` を使っている |
|
|
301
311
|
| `v2.caption-overlap` | warning | 分離した字幕行と袋の写しが同時刻に重なる |
|
|
302
312
|
| `v2.keyframes-ref` | error | `keyframes: { path, count }` の袋が無い / `count` が実数と違う |
|
|
@@ -334,3 +344,7 @@ await p.save(); // 正規直列化 → lint ゲート
|
|
|
334
344
|
- 観察・手術の CLI コマンド(作らない。§5.2-5)
|
|
335
345
|
- 種類ごとに違うグループ(sequence / scene 等)— 「段」と「グループ」の 2 つだけ
|
|
336
346
|
- 文字ごとのキーフレーム(姉妹契約 §8)
|
|
347
|
+
|
|
348
|
+
## 音声片のカット端
|
|
349
|
+
|
|
350
|
+
edit.json v2 の audio lane にある分離済み speech item は、カットで素材端が失われたとき、任意の `cut_edge: { "in": 秒, "out": 秒, "at": 整数フレーム }` を持てる。`in` と `out` はこの音声片自身の元の素材端であり、頭がまたぎで削られた場合だけ `in` が現在の `source.in` と異なり、尻がまたぎで削られた場合だけ `out` が現在の `source.out` と異なる。両端が現在の素材端と等しくなれば欄を消す。`at` は「頭を `cut_edge.in` まで戻したときの開始フレーム」で、現在の `at - round((source.in - cut_edge.in) / 1 フレームぶんの素材秒)` から求める派生値である。カットで音声片を割ったときはそれぞれの片の端だけを保持し、結合時は左片の `in` と右片の `out` を採る。`in` と `out` は 0 以上の有限数で `in < out`、`at` は整数。先行カットのリップルを反映した途中状態では `cut_edge.at` が負になることもある。再生範囲の `source.in/out` と現在位置の `at` はそのまま正本とする。`provenance` はナレーションの生成元専用で、この用途には使わない。欄のない既存 item の意味は変わらない。
|
|
@@ -26,7 +26,7 @@ updated: 2026-09-06
|
|
|
26
26
|
| **L2 アニメーター** | 文字 / 単語 / 行 / 文節ごとのずらし(範囲セレクタ + 量)| アイテムの `animator[]`(パラメータ数個)+ offset のキーフレームは袋 | offset のダイヤだけ(数個)。文字ごとの行は出ない | テロップ・字幕の演出 |
|
|
27
27
|
| **L3 コード** | 断片内の CSS / GSAP アニメ | HTML の中(逃げ道)| 帯だけ(中は不透明)| AI / 上級者 |
|
|
28
28
|
|
|
29
|
-
-
|
|
29
|
+
- **時間と動きの粒度 = 名札の粒度**。部品の中の一部だけ別の時間で動かしたいなら、名札を細かく付けて(文節を部品にして)出す → L1 が効く。位置・回転・箱の上書きは名札なしの要素にも掛けられる(`source.elements`、2026-10-07 オーナー裁定)。それ以上の第 3 の粒度は作らない
|
|
30
30
|
- **文字ごとのキーフレームはデータとして持たない**(100 文字 × 5 プロパティ × 3 点 = 1,500 個 / 文字を直すと全部ずれる / AI が扱えない)。文字単位の動きは L2 で表す
|
|
31
31
|
|
|
32
32
|
## 1. L0 プリセット `motion`
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Material ranges v0
|
|
2
|
+
|
|
3
|
+
The project stores non-destructive source selections in `.akari/material-ranges.json`:
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{ "version": 1, "ranges": { "assets/example.mp4": { "in": 2.5, "out": 8 } } }
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Keys are project-relative media paths. `in` and `out` are seconds in the original media; `out` is greater than `in`. Removing a selection deletes its key. A missing or malformed file means no selections. Updates preserve unrelated top-level keys and other material entries and replace the file by writing a temporary file and renaming it.
|
|
10
|
+
|
|
11
|
+
The selected list strip exposes two handles. Its minimum width is the larger of one second and 24 pixels expressed in source seconds, capped by the media duration. Dragging one handle keeps the other fixed. Double-clicking the strip or pressing × clears the selection. Changes are saved after 150 ms.
|
|
12
|
+
|
|
13
|
+
Material drag payloads and `akari.timeline.addMaterialAtPlayhead` requests may carry `in` and `out`. Drag placement uses the payload; playhead placement reads the saved range. Video and audio placement cap `out` at the media duration and use the full source when no valid range is present. `akari.materials.range.get` and `.set` expose the stored range to other views; changes emit `akari.materials.range.changed` with `{ relativePath, range }` on `window`. Range setters may include `source`; the event preserves it so a view can ignore echoes of its own changes.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# tasks.json v0 — 依頼タスクの運用情報
|
|
2
|
+
|
|
3
|
+
## 位置づけ
|
|
4
|
+
|
|
5
|
+
`.akari/tasks.json` は、AI への依頼の運用情報を持つプロジェクトのサイドカーである。状態、優先度、送信先、戻すための履歴、および注釈以外の依頼を保存する。注釈の本文は `review.json` が正本である。
|
|
6
|
+
|
|
7
|
+
## 置き場
|
|
8
|
+
|
|
9
|
+
プロジェクトの `.akari/tasks.json` に置く。ルート直下には置かない。これはプロジェクト構造契約の「契約サイドカー」に当たる。ファイルは履歴に残す。`.akari/cache/outbox/` の依頼文は再生成できる一時ファイルであり、送信後の記録は tasks.json に残す。
|
|
10
|
+
|
|
11
|
+
## 形
|
|
12
|
+
|
|
13
|
+
top の必須フィールドは `version: 0` と `tasks` 配列である。各タスクの必須フィールドは `id`、`source`、`state`、`createdAt` のみ。`id` は `t-0001` のような連番で、既存の最大番号の次を使う。
|
|
14
|
+
|
|
15
|
+
| フィールド | 型・値 | 用途 |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `source` | `annotation` / `lint` / `proposal` / `export` | タスクの出どころ |
|
|
18
|
+
| `state` | `unsent` / `sent` / `review` / `done` | 進行状態 |
|
|
19
|
+
| `via`, `priority`, `rank` | 文字列、`high` / `normal` / `low`、数値または null | 入口と並び |
|
|
20
|
+
| `outcome` | null または `edited` / `declined` / `failed` / `dismissed` | 結果 |
|
|
21
|
+
| `title`, `body`, `target` | 文字列、文字列、文字列または null | 表示とアンカーのない指示 |
|
|
22
|
+
| `anchor`, `ref` | object または null | 素材時刻と元データへの参照 |
|
|
23
|
+
| `attachments` | `{kind, path, external?}` の配列 | 添付ファイル |
|
|
24
|
+
| `gate`, `needsConfirm` | `auto-ok` / `ask`、真偽値 | 実行前の扱い |
|
|
25
|
+
| `undo` | `{reversible, historyId?, before?, after?, undoneAt?}` | 戻すための記録 |
|
|
26
|
+
| `batchId`, `sentAt`, `sentTo` | 文字列、文字列または null、`{agent, form, route}` | 送信記録 |
|
|
27
|
+
| `response` | null または `{summary, action, respondedAt}` | 応答。`action` は `edited` / `declined` |
|
|
28
|
+
| `createdBy`, `updatedAt`, `orphaned` | `human` / `app` / `ai`、文字列、真偽値 | 作成者と追跡 |
|
|
29
|
+
| `kind`, `targetDetail`, `evidence` | 文字列、object、object | 取り込み時の分類・対象・根拠 |
|
|
30
|
+
| `confidence`, `question`, `risk`, `route` | 文字列 | 確信度、質問、外部送出の危険、担当 |
|
|
31
|
+
| `dependsOn`, `origin`, `mergedFrom` | id 配列、object、id 配列 | 依存、出所、統合元 |
|
|
32
|
+
|
|
33
|
+
`anchor` は `{sourceT, sourceRange}`、`ref` は `{kind, id|key|path|rule}`。`targetDetail` は `refs[]`, `outputT`, `src`, `sourceT`, `cutIndex`, `region`(0〜1 の `[x,y,w,h]`)を持てる。`evidence` は `memo`, `speech`(`[開始秒, 終了秒]`), `quote`, `ink[]`、`origin` は `kind`, `memo`, `job`, `ref` を持てる。`risk: outbound` は `gate: ask` とし、自動実行しない。未知の任意列挙値は警告として扱う。
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"version": 0,
|
|
38
|
+
"tasks": [
|
|
39
|
+
{
|
|
40
|
+
"id": "t-0001",
|
|
41
|
+
"source": "annotation",
|
|
42
|
+
"state": "unsent",
|
|
43
|
+
"createdAt": "2026-10-07T08:10:00.000Z",
|
|
44
|
+
"ref": { "kind": "annotation", "id": "a-0003" },
|
|
45
|
+
"priority": "normal",
|
|
46
|
+
"gate": "ask"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 状態と遷移
|
|
53
|
+
|
|
54
|
+
`unsent → sent → review → done` が基本形。リントの再検査で指摘が消えたときのみ `sent → done` とできる。人による再依頼・復元は `review → unsent`。人の無視は任意の状態から `done` とし、`outcome: dismissed` を記録する。AI は `done` に進めない。
|
|
55
|
+
|
|
56
|
+
## 注釈との関係
|
|
57
|
+
|
|
58
|
+
`ref.kind: annotation` の本文、対象、時刻、ペンの線は `review.json` を正本とする。tasks.json は運用情報を重ねる。注釈の `open` は `unsent`、`addressed` は `review`、`resolved` は `done` に写す。`open` でも送信記録が `sent` なら `sent` を保つ。その他の食い違いは `review.json` を優先する。参照先の注釈が消えたタスクは `orphaned: true` として残す。`review.json` の形は変更しない。
|
|
59
|
+
|
|
60
|
+
## 読み手の規約
|
|
61
|
+
|
|
62
|
+
ファイルが無ければ空の `version: 0` として読む。未知フィールドを保持し、未知の `source` / `state` は表示だけして変更しない。必須フィールドの欠落は警告に残し、タスクを捨てない。JSON が壊れたファイルは上書きしない。`version > 0` は「新しい形式です。スキル / アプリを更新してください」として拒否する。
|
|
63
|
+
|
|
64
|
+
## 書き手の規約
|
|
65
|
+
|
|
66
|
+
`.akari/tasks.json.lock` ディレクトリを確保し、ロック内でファイル全文を読み直す。60 秒を超えた古いロックは回収する。`tasks.json.tmp` に 2 スペース整形・末尾改行で書き、rename で原子的に置き換える。注釈の参照があるタスクの本文は書き写さない。状態の変更ごとに個別の git commit は作らない。
|
|
67
|
+
|
|
68
|
+
## 検証
|
|
69
|
+
|
|
70
|
+
`node packages/schemas/bin/validate-tasks.mjs <tasks.json>` を実行する。形式と意味のエラーは終了コード 1、警告のみなら 0、引数の誤りは 2。id の重複と `risk: outbound` + `gate: auto-ok` はエラーである。
|
|
71
|
+
|
|
72
|
+
## 互換
|
|
73
|
+
|
|
74
|
+
古いアプリやスキルは `review.json` をそのまま使える。tasks.json が無いプロジェクトでも注釈から仮想タスクを作る。「送った」注釈 id の既存保存は初回に取り込める。`.akari/tasks.json` は保持対象、ロックと `.akari/cache/outbox/` は一時データとして扱う。
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# AKARI バイブの耳
|
|
2
|
+
|
|
3
|
+
耳はマイクを開き、音声を文字にして辞書を適用する共有部品です。判断サービスの資格や鍵に依存せず、マイクの持ち主を 1 つにします。
|
|
4
|
+
|
|
5
|
+
段階 A では、試し聞きとメモ、区画の「いま」が耳を使います。Jev は従来どおり自分のマイクを使い、同時利用は禁止します。Jev 動作中は耳を使用中として扱います。
|
|
6
|
+
|
|
7
|
+
段階 B では、Jev の聞き取りも耳へ統一します。発話と音量を係の `/say` と `/level` へ渡し、マイクの持ち主を耳に一本化します。
|
|
8
|
+
|
|
9
|
+
現時点は骨組みのみで、実装は今後行います。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0-beta.2",
|
|
4
4
|
"description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。 [akari-video npm vendor: bin/akari.mjs is reference-only. These CLI entrypoints are not included in the akari-video npm package. Use `akari doctor --json` and run the path reported in `render_cut.path`. Full installations provide it in a monorepo checkout, ~/.akari/app, /Applications/AKARI Video.app/Contents/Resources/packages, or %LOCALAPPDATA%\\Programs\\@akari-videoshell\\resources\\packages.]",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// ~/.akari/catalog-cache.json へ自動キャッシュ(オフライン時のフォールバック)。
|
|
3
3
|
// ローカルパス指定(開発・テスト)はファイルをそのまま読む。
|
|
4
4
|
|
|
5
|
-
import { readFile, mkdir, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { readFile, mkdir, writeFile, stat } from 'node:fs/promises';
|
|
6
6
|
import { catalogCachePath, resolveAkariHome, resolveCatalogSource } from './env.mjs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { loadInstalledItems, mergeInstalledItems } from './installed.mjs';
|
|
@@ -16,9 +16,26 @@ function normalizeCatalog(catalog) {
|
|
|
16
16
|
return catalog;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// The local catalog is large; keep the parsed value until the file changes.
|
|
20
|
+
const parsedCatalogs = new Map();
|
|
21
|
+
async function readLocalCatalog(file, visible = false) {
|
|
22
|
+
const info = await stat(file);
|
|
23
|
+
const cached = parsedCatalogs.get(file);
|
|
24
|
+
if (cached && cached.mtimeMs === info.mtimeMs && cached.ctimeMs === info.ctimeMs
|
|
25
|
+
&& cached.size === info.size) {
|
|
26
|
+
if (visible && !cached.public) cached.public = publicCatalog(cached.catalog);
|
|
27
|
+
return visible ? cached.public : cached.catalog;
|
|
28
|
+
}
|
|
29
|
+
const catalog = normalizeCatalog(JSON.parse(await readFile(file, 'utf8')));
|
|
30
|
+
const entry = { mtimeMs: info.mtimeMs, ctimeMs: info.ctimeMs, size: info.size, catalog,
|
|
31
|
+
...(visible ? { public: publicCatalog(catalog) } : {}) };
|
|
32
|
+
parsedCatalogs.set(file, entry);
|
|
33
|
+
return visible ? entry.public : catalog;
|
|
34
|
+
}
|
|
35
|
+
|
|
19
36
|
export async function readCatalogCache(env = process.env) {
|
|
20
37
|
try {
|
|
21
|
-
return
|
|
38
|
+
return await readLocalCatalog(catalogCachePath(env), true);
|
|
22
39
|
} catch {
|
|
23
40
|
return null;
|
|
24
41
|
}
|
|
@@ -53,6 +70,7 @@ export async function cacheCatalog(env = process.env, catalog) {
|
|
|
53
70
|
const home = resolveAkariHome(env);
|
|
54
71
|
await mkdir(home, { recursive: true });
|
|
55
72
|
await writeFile(catalogCachePath(env), `${JSON.stringify(publicCatalog(normalizeCatalog(catalog)), null, 2)}\n`);
|
|
73
|
+
parsedCatalogs.delete(catalogCachePath(env));
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
/**
|
|
@@ -70,8 +88,7 @@ export async function loadCatalog({ env = process.env, fetchImpl = fetch, includ
|
|
|
70
88
|
}
|
|
71
89
|
|
|
72
90
|
if (source.kind === 'file') {
|
|
73
|
-
|
|
74
|
-
catalog = normalizeCatalog(JSON.parse(raw));
|
|
91
|
+
catalog = await readLocalCatalog(source.value);
|
|
75
92
|
} else {
|
|
76
93
|
try {
|
|
77
94
|
const { response: res, controller } = await fetchTimed(source.value, { fetchImpl, timeouts, label: 'カタログ' });
|
|
@@ -157,8 +157,13 @@ export async function readProjectReferences(projectDir) {
|
|
|
157
157
|
|
|
158
158
|
export async function recordProjectReference(projectDir, reference) {
|
|
159
159
|
assertReference(reference);
|
|
160
|
+
const alreadyRecorded = references => references.some(entry =>
|
|
161
|
+
entry.id === reference.id && entry.category === reference.category);
|
|
162
|
+
const current = await readProjectReferences(projectDir);
|
|
163
|
+
if (alreadyRecorded(current)) return current;
|
|
160
164
|
return withReferencesLock(projectDir, async () => {
|
|
161
165
|
const references = await readProjectReferences(projectDir);
|
|
166
|
+
if (alreadyRecorded(references)) return references;
|
|
162
167
|
references.push({ id: reference.id, category: reference.category });
|
|
163
168
|
const normalized = normalizeReferences(references);
|
|
164
169
|
await writeProjectReferences(projectDir, normalized);
|
|
@@ -12,7 +12,7 @@ import { resolveAssetLibraryRoots } from '../../creator-root/src/index.mjs';
|
|
|
12
12
|
|
|
13
13
|
import { spawn } from 'node:child_process';
|
|
14
14
|
import { constants } from 'node:fs';
|
|
15
|
-
import { cp, mkdir, mkdtemp, readFile, rename, rm, realpath, stat, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { cp, lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, realpath, stat, writeFile } from 'node:fs/promises';
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
18
18
|
import { format } from 'node:util';
|
|
@@ -94,6 +94,52 @@ async function validateAsset(assetDir) {
|
|
|
94
94
|
|
|
95
95
|
export { AssetResolverError };
|
|
96
96
|
|
|
97
|
+
// overlay-runtime は resolver の依存にしない。断片ルートの変換規則は両方を同じテストで固定する。
|
|
98
|
+
export function withoutFragmentRootTiming(source, { preserveNaturalDuration = false } = {}) {
|
|
99
|
+
let cursor = 0;
|
|
100
|
+
while (cursor < source.length) {
|
|
101
|
+
const start = source.indexOf('<', cursor);
|
|
102
|
+
if (start < 0) break;
|
|
103
|
+
if (source.startsWith('<!--', start)) {
|
|
104
|
+
const end = source.indexOf('-->', start + 4);
|
|
105
|
+
if (end < 0) throw new Error('HTML コメントが閉じていません');
|
|
106
|
+
cursor = end + 3;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const tag = /^<([A-Za-z][\w:-]*)(?:"[^"]*"|'[^']*'|[^'">])*>/u.exec(source.slice(start));
|
|
110
|
+
if (!tag) { cursor = start + 1; continue; }
|
|
111
|
+
if (/^(?:style|script)$/iu.test(tag[1])) {
|
|
112
|
+
const closing = new RegExp(`</${tag[1]}\\s*>`, 'iu').exec(source.slice(start + tag[0].length));
|
|
113
|
+
if (!closing) throw new Error('HTML の前置要素が閉じていません');
|
|
114
|
+
cursor = start + tag[0].length + closing.index + closing[0].length;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (/^link$/iu.test(tag[1])) { cursor = start + tag[0].length; continue; }
|
|
118
|
+
const nameEnd = tag[1].length + 1;
|
|
119
|
+
const attributes = tag[0].slice(nameEnd, -1);
|
|
120
|
+
const tokens = /([^\s=/>]+)(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?/gu;
|
|
121
|
+
const removals = [];
|
|
122
|
+
let duration = null;
|
|
123
|
+
let hasNaturalDuration = false;
|
|
124
|
+
for (const token of attributes.matchAll(tokens)) {
|
|
125
|
+
if (preserveNaturalDuration && /^data-akari-natural-duration$/iu.test(token[1])) hasNaturalDuration = true;
|
|
126
|
+
if (!/^data-(?:start|duration)$/iu.test(token[1])) continue;
|
|
127
|
+
if (preserveNaturalDuration && /^data-duration$/iu.test(token[1])) {
|
|
128
|
+
const value = token[0].match(/=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/u)?.slice(1).find(part => part !== undefined);
|
|
129
|
+
if (value !== undefined && Number.isFinite(Number(value)) && Number(value) > 0) duration = value;
|
|
130
|
+
}
|
|
131
|
+
let from = token.index;
|
|
132
|
+
while (from > 0 && /\s/u.test(attributes[from - 1])) from--;
|
|
133
|
+
removals.push([from, token.index + token[0].length]);
|
|
134
|
+
}
|
|
135
|
+
let changed = attributes;
|
|
136
|
+
for (const [from, to] of removals.reverse()) changed = changed.slice(0, from) + changed.slice(to);
|
|
137
|
+
if (preserveNaturalDuration && !hasNaturalDuration && duration !== null) changed += ` data-akari-natural-duration="${duration}"`;
|
|
138
|
+
return source.slice(0, start + nameEnd) + changed + source.slice(start + tag[0].length - 1);
|
|
139
|
+
}
|
|
140
|
+
throw new Error('HTML 断片のルート要素がありません');
|
|
141
|
+
}
|
|
142
|
+
|
|
97
143
|
/** category/id を優先し、bare id はカタログ内で一意な場合だけ解決する。 */
|
|
98
144
|
export function findCatalogItem(catalog, ref) {
|
|
99
145
|
if (typeof ref === 'string' && ref.includes('/')) {
|
|
@@ -132,6 +178,12 @@ export async function copyIntoProject(sourceDir, projectDir, category, id) {
|
|
|
132
178
|
child.on('close', code => resolveResult(code === 0));
|
|
133
179
|
});
|
|
134
180
|
if (cloned) {
|
|
181
|
+
try {
|
|
182
|
+
await normalizeCopiedFragments(dest, category);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
await rm(dest, { recursive: true, force: true });
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
135
187
|
return dest;
|
|
136
188
|
}
|
|
137
189
|
// クロスボリューム等で -c が失敗したケース。部分的に書かれた dest を掃除してから
|
|
@@ -142,9 +194,41 @@ export async function copyIntoProject(sourceDir, projectDir, category, id) {
|
|
|
142
194
|
// COPYFILE_FICLONE(_FORCE ではない): 対応 FS(APFS 等)では CoW クローンで実体化コピーを
|
|
143
195
|
// 省略し、非対応環境では黙って通常コピーへフォールバックする(失敗しない)。
|
|
144
196
|
await cp(realSourceDir, dest, { recursive: true, mode: constants.COPYFILE_FICLONE });
|
|
197
|
+
try {
|
|
198
|
+
await normalizeCopiedFragments(dest, category);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
await rm(dest, { recursive: true, force: true });
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
145
203
|
return dest;
|
|
146
204
|
}
|
|
147
205
|
|
|
206
|
+
async function normalizeCopiedFragments(dest, category) {
|
|
207
|
+
if (category !== 'overlay' && category !== 'scene3d') return;
|
|
208
|
+
const files = ['fragment.html'];
|
|
209
|
+
const variants = path.join(dest, 'variants');
|
|
210
|
+
const variantInfo = await lstat(variants).catch(error => {
|
|
211
|
+
if (error.code === 'ENOENT') return null;
|
|
212
|
+
throw error;
|
|
213
|
+
});
|
|
214
|
+
if (variantInfo && !variantInfo.isDirectory()) throw new Error('variants が通常のディレクトリではありません');
|
|
215
|
+
for (const entry of variantInfo ? await readdir(variants) : []) {
|
|
216
|
+
if (entry.endsWith('.html')) files.push(path.join('variants', entry));
|
|
217
|
+
}
|
|
218
|
+
for (const file of files) {
|
|
219
|
+
const target = path.join(dest, file);
|
|
220
|
+
const info = await lstat(target).catch(error => {
|
|
221
|
+
if (error.code === 'ENOENT') return null;
|
|
222
|
+
throw error;
|
|
223
|
+
});
|
|
224
|
+
if (info === null) continue;
|
|
225
|
+
if (!info.isFile()) throw new Error(`断片ファイルが通常のファイルではありません: ${file}`);
|
|
226
|
+
const source = await readFile(target, 'utf8');
|
|
227
|
+
const cleaned = withoutFragmentRootTiming(source, { preserveNaturalDuration: true });
|
|
228
|
+
if (cleaned !== source) await writeFile(target, cleaned);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
148
232
|
async function moveIntoLibrary(tempDir, destDir) {
|
|
149
233
|
await mkdir(path.dirname(destDir), { recursive: true });
|
|
150
234
|
if (await stat(destDir).then(() => true, error => { if (error.code === 'ENOENT') return false; throw error; })) {
|
|
@@ -282,6 +366,24 @@ export async function resolve(
|
|
|
282
366
|
{ env = process.env, fetchImpl = fetch, project = null, force = false, reference = false, timeouts } = {},
|
|
283
367
|
) {
|
|
284
368
|
const home = resolveAssetLibraryRoots(env).write;
|
|
369
|
+
// A qualified reference already names the local directory. Avoid parsing the
|
|
370
|
+
// large catalog on the common placement path; uncached and bare IDs still use it.
|
|
371
|
+
const localRef = typeof ref === 'string' ? ref.split('/') : [];
|
|
372
|
+
const qualified = localRef.length === 2 && localRef.every(part => part.length > 0
|
|
373
|
+
&& part !== '.' && part !== '..' && !/[\\\0]/.test(part));
|
|
374
|
+
if (!force && qualified) {
|
|
375
|
+
const cachedDir = await cachedAssetDirAsync(env, localRef[0], localRef[1]);
|
|
376
|
+
if (cachedDir) {
|
|
377
|
+
const result = { id: localRef[1], category: localRef[0], dir: cachedDir, cached: true };
|
|
378
|
+
if (project && reference) {
|
|
379
|
+
await recordProjectReference(project, { id: localRef[1], category: localRef[0] });
|
|
380
|
+
result.referenced = true;
|
|
381
|
+
} else if (project) {
|
|
382
|
+
result.projectDir = await copyIntoProject(cachedDir, project, localRef[0], localRef[1]);
|
|
383
|
+
}
|
|
384
|
+
return result;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
285
387
|
const catalog = await loadCatalogForResolveAsync(ref, { env, fetchImpl, timeouts });
|
|
286
388
|
const item = findCatalogItem(catalog, ref);
|
|
287
389
|
|
|
@@ -82,7 +82,7 @@ export async function composeState({ env = process.env, fetchImpl = fetch, inten
|
|
|
82
82
|
localItems.delete(key);
|
|
83
83
|
// Preserve remote download descriptors/preview keys for existing consumers.
|
|
84
84
|
// Local-only assets use the actual directory listing assembled above.
|
|
85
|
-
return local ? { ...item, ...local, tier: item.tier ?? assetTier(item), files: item.files ?? local.files, preview: item.preview ?? local.preview,
|
|
85
|
+
return local ? { ...item, ...local, tier: item.tier ?? assetTier(item), files: item.files ?? local.files, preview: local.libraryDir && local.preview ? local.preview : item.preview ?? local.preview,
|
|
86
86
|
...(item.source === 'installed' ? { source: item.source } : {}) } : item;
|
|
87
87
|
});
|
|
88
88
|
const localOnlyKeys = new Set(localItems.keys());
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { mkdirSync, readFileSync, utimesSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import test from 'node:test';
|
|
5
5
|
import { cacheCatalog, loadCatalog, readCatalogCache } from '../src/catalog.mjs';
|
|
@@ -13,6 +13,18 @@ test('loadCatalog はローカルパス指定のカタログを読める', async
|
|
|
13
13
|
assert.equal(loaded.items.length, catalog.items.length);
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
+
test('ローカルカタログは mtime が同じ間は解析結果を再利用し、更新後に読み直す', async () => {
|
|
17
|
+
const { env, catalog, catalogPath } = setupFixtureEnv();
|
|
18
|
+
const first = await loadCatalog({ env, includeInstalled: false });
|
|
19
|
+
assert.equal(await loadCatalog({ env, includeInstalled: false }), first);
|
|
20
|
+
writeFileSync(catalogPath, JSON.stringify({ ...catalog, items: [...catalog.items, { id: 'new', category: 'still' }] }));
|
|
21
|
+
const updated = new Date(Date.now() + 5000);
|
|
22
|
+
utimesSync(catalogPath, updated, updated);
|
|
23
|
+
const next = await loadCatalog({ env, includeInstalled: false });
|
|
24
|
+
assert.notEqual(next, first);
|
|
25
|
+
assert.equal(next.items.at(-1).id, 'new');
|
|
26
|
+
});
|
|
27
|
+
|
|
16
28
|
test('catalog cache drops files for locked and Pro items before writing and on old-cache reads', async () => {
|
|
17
29
|
const { env, home, catalog } = setupFixtureEnv();
|
|
18
30
|
catalog.items[0].tier = 'free';
|
|
@@ -88,6 +100,19 @@ test('composeState: ローカルに実体があるものは cached になる', a
|
|
|
88
100
|
assert.equal(free.state, 'cached');
|
|
89
101
|
});
|
|
90
102
|
|
|
103
|
+
test('composeState: cached preview uses local filename over catalog key', async () => {
|
|
104
|
+
const { env, home, catalog, catalogPath } = setupFixtureEnv();
|
|
105
|
+
catalog.items[0].preview = 'still/mini-still/v1/preview.png';
|
|
106
|
+
writeFileSync(catalogPath, JSON.stringify(catalog));
|
|
107
|
+
const dir = path.join(home, 'assets', 'still', 'mini-still');
|
|
108
|
+
mkdirSync(dir, { recursive: true });
|
|
109
|
+
writeFileSync(path.join(dir, 'preview.png'), 'local preview');
|
|
110
|
+
const item = (await composeState({ env })).items.find(entry => entry.id === 'mini-still');
|
|
111
|
+
assert.equal(item.state, 'cached');
|
|
112
|
+
assert.equal(item.preview, 'preview.png');
|
|
113
|
+
assert.equal(item.libraryDir, dir);
|
|
114
|
+
});
|
|
115
|
+
|
|
91
116
|
test('composeState: legacy local-only metadata without tier displays free while catalog stays fail-closed', async () => {
|
|
92
117
|
const { env, home, catalog, catalogPath } = setupFixtureEnv();
|
|
93
118
|
const dir = path.join(home, 'assets', 'audio', 'old-local');
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"cases": [
|
|
4
|
+
{
|
|
5
|
+
"name": "comment and child timing",
|
|
6
|
+
"source": "<!-- data-duration=\"90\" -->\n<div data-start=\"0\" data-duration=\"8\"><span data-duration=\"2\">text</span></div>",
|
|
7
|
+
"plain": "<!-- data-duration=\"90\" -->\n<div><span data-duration=\"2\">text</span></div>",
|
|
8
|
+
"preserved": "<!-- data-duration=\"90\" -->\n<div data-akari-natural-duration=\"8\"><span data-duration=\"2\">text</span></div>"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"name": "leading style and existing natural duration",
|
|
12
|
+
"source": "<style>.x{color:red}</style><div data-start=0 data-duration=6 data-akari-natural-duration=\"9\">text</div>",
|
|
13
|
+
"plain": "<style>.x{color:red}</style><div data-akari-natural-duration=\"9\">text</div>",
|
|
14
|
+
"preserved": "<style>.x{color:red}</style><div data-akari-natural-duration=\"9\">text</div>"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "duration before class",
|
|
18
|
+
"source": "<div data-start=\"0\" data-duration=\"4\" class=\"x\">text</div>",
|
|
19
|
+
"plain": "<div class=\"x\">text</div>",
|
|
20
|
+
"preserved": "<div class=\"x\" data-akari-natural-duration=\"4\">text</div>"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "start only with child duration",
|
|
24
|
+
"source": "<div data-start=\"3\"><span data-duration=\"2\">text</span></div>",
|
|
25
|
+
"plain": "<div><span data-duration=\"2\">text</span></div>",
|
|
26
|
+
"preserved": "<div><span data-duration=\"2\">text</span></div>"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "single quoted duration",
|
|
30
|
+
"source": "<div data-duration='2.5' data-start='0'>text</div>",
|
|
31
|
+
"plain": "<div>text</div>",
|
|
32
|
+
"preserved": "<div data-akari-natural-duration=\"2.5\">text</div>"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|