akari-video 0.1.99 → 0.1.100

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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/src/internal-command.mjs +1 -1
  3. package/src/repo-assets.mjs +1 -1
  4. package/vendor/docs/contract-2026-08-02-preview-parity.md +4 -2
  5. package/vendor/docs/contract-2026-09-02-asset-reference-model.md +2 -2
  6. package/vendor/packages/akari-launcher/package.json +1 -1
  7. package/vendor/packages/audio-library-setup/bin/beat-grid.mjs +16 -5
  8. package/vendor/packages/creator-root/src/library-reference.mjs +80 -0
  9. package/vendor/packages/creator-root/test/library-reference.test.mjs +163 -0
  10. package/vendor/packages/edit-lint/src/edit-lint.mjs +44 -0
  11. package/vendor/packages/edit-lint/src/library-reference.mjs +1 -97
  12. package/vendor/packages/edit-lint/src/license-axes.mjs +1 -41
  13. package/vendor/packages/edit-store/lib/caption-display.js +2 -2
  14. package/vendor/packages/edit-store/lib/caption-sample-text.d.ts +2 -0
  15. package/vendor/packages/edit-store/lib/caption-sample-text.js +5 -0
  16. package/vendor/packages/edit-store/lib/index.d.ts +1 -0
  17. package/vendor/packages/edit-store/lib/index.js +1 -0
  18. package/vendor/packages/edit-store/lib/track-z.js +1 -1
  19. package/vendor/packages/edit-store/lib/webview-kernel.js +1 -0
  20. package/vendor/packages/media-bin/scripts/build-whisper.mjs +12 -2
  21. package/vendor/packages/media-bin/scripts/fetch-binaries.mjs +12 -2
  22. package/vendor/packages/overlay-runtime/src/interaction.css +1 -0
  23. package/vendor/packages/overlay-runtime/src/interaction.js +30 -5
  24. package/vendor/packages/overlay-runtime/src/three-runtime.js +25 -4
  25. package/vendor/packages/preview-server/package.json +1 -1
  26. package/vendor/packages/render-cut/src/library-reference.mjs +1 -97
  27. package/vendor/packages/schemas/asset-meta.schema.json +7 -0
  28. package/vendor/packages/schemas/bin/validate-asset.mjs +6 -0
  29. package/vendor/packages/schemas/test/asset-meta-default.test.mjs +43 -0
  30. package/vendor/skills/analyze-footage/bin/face-expression/face-expression.mjs +12 -2
  31. package/vendor/skills/beat-sync-edit/verify-loop.md +1 -0
  32. package/vendor/skills/edit-plan/bin/generated/contract-validators.cjs +2511 -557
  33. package/vendor/skills/edit-plan/bin/test/contract-validators-regen.test.mjs +46 -0
  34. package/vendor/skills/overlay-authoring/3d.md +2 -1
  35. package/vendor/skills/setup-chat-approval/bin/test/distribution.test.mjs +3 -1
  36. package/vendor/templates/project-default/AGENTS.md +2 -3
  37. package/vendor/templates/project-default/CLAUDE.md +2 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.99",
3
+ "version": "0.1.100",
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": {
@@ -40,7 +40,7 @@ export async function runInternalCommand(args, options = {}) {
40
40
  const definitions = {
41
41
  'beat-sync-beatmap': { path: assets.beatmapScript, node: true },
42
42
  'beat-sync-probe-frame': { path: assets.probeFrameScript, node: true },
43
- 'beat-sync-render-when-idle': { path: assets.renderWhenIdleScript, node: false },
43
+ 'beat-sync-render-when-idle': { path: assets.renderWhenIdleScript, node: true },
44
44
  'vision-finger-frame': {
45
45
  path: fingerFrameScript && existsSync(fingerFrameScript) ? fingerFrameScript : null,
46
46
  node: true
@@ -37,7 +37,7 @@ export const DECISION_LOG_SCRIPT_RELATIVE = path.join('packages', 'akari-tools',
37
37
  export const CAPTIONS_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'captions.mjs');
38
38
  // capture-command.mjs のエラー文と apps/shell の同梱テストが同じ相対パスを名指しできるよう export する。
39
39
  export const CAPTURE_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'capture.mjs');
40
- export const RENDER_WHEN_IDLE_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'render-when-idle.sh');
40
+ export const RENDER_WHEN_IDLE_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'render-when-idle.mjs');
41
41
  export const EYE_BAR_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'eye-bar.mjs');
42
42
  export const FINGER_FRAME_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'finger-frame.mjs');
43
43
  export const MEDIA_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'media.mjs');
@@ -283,8 +283,10 @@ legacy 合成経路は、移行中の既存利用者と Windows を支えるた
283
283
  | 1 行の文字数予算 | 20 | 10 |
284
284
  | 無指定で複数行になる字幕 | 全行を静的表示 | `words[]` があれば reveal へ自動昇格し、無ければ静的表示 |
285
285
 
286
- - 行分割は **句読点 → 空白 → 文節境界 → 文字上限**の順に候補を選ぶ。word の途中では折り返さず、
287
- 最寄りの word 境界へスナップする。
286
+ - 行分割は **明示改行 → 「。」の直後(行末を除く)**で区切る。各区間が文字数予算を超える場合だけ、
287
+ **「、」の直後(予算内で最も後ろ)→ 空白 → 文節境界 → 文字上限**の順に折り返し候補を選ぶ。
288
+ 予算内なら「、」を含んでも 1 行のままとし、word の途中の分割点は最寄りの word 境界へスナップする。
289
+ 2026-10-02 改訂: オーナー指示により、「、」で短い字幕が 2 行になるのをやめる。
288
290
  - style、サイズ、行数、表示方式などの明示指定は、常に上記の既定より優先する。
289
291
 
290
292
  ### 5.2 overlays の解決
@@ -36,8 +36,8 @@
36
36
  - render-cut は解決した入力を render inputs 記録に `scope: "library"` として残す
37
37
  (既存 `scope: "akari"` と同列の additive 記録)。採用した実ルートを `library_root` に保存して後段でも検査する。edit-lint は解決できる参照を欠落と報告せず、
38
38
  台帳にあるが実体が無い参照は「共有ライブラリ参照(未取得)」として欠落報告する。
39
- - render-cut / edit-lint は依存ゼロ CLI のため、解決ロジックは各パッケージ内に**同一実装を重複**して
40
- 持つ(`src/library-reference.mjs`)。挙動同一性は両テストの同一ケース表で担保する。
39
+ - 素材ライブラリ参照の解決ロジックの正本は `packages/creator-root/src/library-reference.mjs` に置く。
40
+ render-cut / edit-lint は各 `src/library-reference.mjs` から正本を再 export する。
41
41
 
42
42
  ## 3. 使い方(CLI)
43
43
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.99",
3
+ "version": "0.1.100",
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": [
@@ -19,12 +19,18 @@
19
19
 
20
20
  import { audioReadPath, readAudioDeclarations } from '../shared/library-roots.mjs';
21
21
  import { resolveAssetLibraryRoots } from '../../creator-root/src/index.mjs';
22
+ import { resolveFfprobe } from '../../media-bin/src/index.mjs';
22
23
  import { existsSync } from 'node:fs';
23
24
  import { readFile } from 'node:fs/promises';
24
25
  import { spawnSync } from 'node:child_process';
25
26
  import path from 'node:path';
26
27
  import { cutCandidates, musicGrid, snapToGrid, toFrameGrid } from '../shared/beat-grid.mjs';
27
28
 
29
+ const ffprobeBinary = (() => {
30
+ try { return { command: resolveFfprobe() }; }
31
+ catch (error) { return { error }; }
32
+ })();
33
+
28
34
  function libraryRoot(env = process.env) {
29
35
  return path.join(resolveAssetLibraryRoots(env).write, 'audio');
30
36
  }
@@ -91,11 +97,16 @@ function trackIdFromPath(bgmPath) {
91
97
  }
92
98
 
93
99
  function probeDuration(filePath) {
94
- const result = spawnSync('ffprobe', [
95
- '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=nw=1:nk=1', filePath,
96
- ], { encoding: 'utf8' });
97
- const seconds = Number(String(result.stdout ?? '').trim());
98
- return Number.isFinite(seconds) && seconds > 0 ? seconds : null;
100
+ try {
101
+ if (Object.hasOwn(ffprobeBinary, 'error')) throw ffprobeBinary.error;
102
+ const result = spawnSync(ffprobeBinary.command, [
103
+ '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=nw=1:nk=1', filePath,
104
+ ], { encoding: 'utf8' });
105
+ const seconds = Number(String(result.stdout ?? '').trim());
106
+ return Number.isFinite(seconds) && seconds > 0 ? seconds : null;
107
+ } catch {
108
+ return null;
109
+ }
99
110
  }
100
111
 
101
112
  function formatHuman(grid, { trackId, snaps, cuts, declarationsSource }) {
@@ -0,0 +1,80 @@
1
+ import { lstatSync, readFileSync, realpathSync } from "node:fs";
2
+ import { isAbsolute, join, relative, resolve } from "node:path";
3
+ import { resolveAssetLibraryRoots } from "./index.mjs";
4
+
5
+ export { resolveAssetLibraryRoots };
6
+
7
+ // 参照台帳(.akari/asset-references.json)のスキーマ版数。edit.json の version とは無関係。
8
+ const REFERENCES_SCHEMA_VERSION = 0;
9
+
10
+ export function readProjectReferences(projectRoot) {
11
+ try {
12
+ const parsed = JSON.parse(readFileSync(
13
+ join(projectRoot, ".akari", "asset-references.json"),
14
+ "utf8",
15
+ ));
16
+ if (parsed?.version !== REFERENCES_SCHEMA_VERSION || !Array.isArray(parsed.references)) return [];
17
+ return parsed.references.filter((entry) => entry !== null
18
+ && typeof entry === "object"
19
+ && !Array.isArray(entry)
20
+ && typeof entry.id === "string"
21
+ && entry.id.length > 0
22
+ && typeof entry.category === "string"
23
+ && entry.category.length > 0)
24
+ .map((entry) => ({ id: entry.id, category: entry.category }));
25
+ } catch {
26
+ return [];
27
+ }
28
+ }
29
+
30
+ export function resolveAkariAssetsDir(env = process.env) {
31
+ return resolveAssetLibraryRoots(env).write;
32
+ }
33
+
34
+ function declaredAssetParts(projectRoot, declaredPath) {
35
+ if (typeof declaredPath !== "string" || declaredPath.length === 0) return null;
36
+ const rawSegments = declaredPath.replaceAll("\\", "/").split("/");
37
+ if (rawSegments.some((segment) => segment === "." || segment === "..")) return null;
38
+ const root = resolve(projectRoot);
39
+ const lexical = isAbsolute(declaredPath) ? resolve(declaredPath) : resolve(root, declaredPath);
40
+ if (!isWithin(root, lexical)) return null;
41
+ const projectRelative = relative(root, lexical).replaceAll("\\", "/");
42
+ const segments = projectRelative.split("/");
43
+ if (segments.length < 4
44
+ || segments[0] !== "assets"
45
+ || segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
46
+ return null;
47
+ }
48
+ return { category: segments[1], id: segments[2], rest: segments.slice(3) };
49
+ }
50
+
51
+ export function resolveLibraryFallback({
52
+ projectRoot,
53
+ declaredPath,
54
+ references = readProjectReferences(projectRoot),
55
+ akariAssetsDir,
56
+ libraryRoots = akariAssetsDir ? [akariAssetsDir] : resolveAssetLibraryRoots().read,
57
+ }) {
58
+ const parts = declaredAssetParts(projectRoot, declaredPath);
59
+ if (!parts || !references.some(
60
+ (entry) => entry.category === parts.category && entry.id === parts.id,
61
+ )) return { matched: false, path: null, libraryRoot: null };
62
+
63
+ for (const root of libraryRoots) {
64
+ const lexicalRoot = resolve(root);
65
+ const lexicalTarget = resolve(lexicalRoot, parts.category, parts.id, ...parts.rest);
66
+ if (!isWithin(lexicalRoot, lexicalTarget)) continue;
67
+ try {
68
+ const actualRoot = realpathSync(lexicalRoot);
69
+ const actualTarget = realpathSync(lexicalTarget);
70
+ if (!isWithin(actualRoot, actualTarget) || !lstatSync(actualTarget).isFile()) continue;
71
+ return { matched: true, path: actualTarget, libraryRoot: actualRoot };
72
+ } catch { /* Try the previous location during migration. */ }
73
+ }
74
+ return { matched: true, path: null, libraryRoot: null };
75
+ }
76
+
77
+ function isWithin(root, target) {
78
+ const value = relative(root, target);
79
+ return value === "" || (!value.startsWith("..") && !isAbsolute(value));
80
+ }
@@ -0,0 +1,163 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import fs from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { resolveAkariAssetsDir, resolveAssetLibraryRoots, resolveLibraryFallback } from '../src/library-reference.mjs';
7
+
8
+ const CASES = [
9
+ { name: 'new wins', files: [true, true], winner: 0 },
10
+ { name: 'old fallback', files: [false, true], winner: 1 },
11
+ { name: 'new only', files: [true, false], winner: 0 },
12
+ { name: 'missing', files: [false, false], winner: null },
13
+ { name: 'no ledger', files: [true, true], winner: null, references: [] },
14
+ { name: 'traversal both roots', files: [true, true], winner: null, declaredPath: 'assets/still/card/../card/frame.png' },
15
+ { name: 'outside symlink new', files: ['escape', false], winner: null },
16
+ { name: 'outside symlink old', files: [false, 'escape'], winner: null },
17
+ { name: 'outside symlink both', files: ['escape', 'escape'], winner: null },
18
+ { name: 'invalid new still tries old', files: ['escape', true], winner: 1 },
19
+ { name: 'directory is not a file', files: ['directory', 'directory'], winner: null },
20
+ ];
21
+
22
+ for (const entry of CASES) test(entry.name, async t => {
23
+ const temp = await fs.mkdtemp(path.join(tmpdir(), 'library-roots-'));
24
+ t.after(() => fs.rm(temp, { recursive: true, force: true }));
25
+ const roots = [path.join(temp, 'library'), path.join(temp, 'home/assets')];
26
+ const outside = path.join(temp, 'outside');
27
+ await fs.writeFile(outside, 'outside');
28
+ for (let index = 0; index < roots.length; index++) {
29
+ const file = path.join(roots[index], 'still/card/frame.png');
30
+ await fs.mkdir(path.dirname(file), { recursive: true });
31
+ if (entry.files[index] === 'escape') await fs.symlink(outside, file);
32
+ else if (entry.files[index] === 'directory') await fs.mkdir(file);
33
+ else if (entry.files[index]) await fs.writeFile(file, String(index));
34
+ }
35
+ const result = resolveLibraryFallback({
36
+ projectRoot: path.join(temp, 'project'),
37
+ declaredPath: entry.declaredPath ?? 'assets/still/card/frame.png',
38
+ references: entry.references ?? [{ category: 'still', id: 'card' }],
39
+ libraryRoots: roots,
40
+ });
41
+ assert.equal(result.matched, !((entry.references?.length === 0) || entry.declaredPath));
42
+ if (entry.winner === null) {
43
+ assert.equal(result.path, null);
44
+ assert.equal(result.libraryRoot, null);
45
+ } else {
46
+ assert.equal(result.path, await fs.realpath(path.join(roots[entry.winner], 'still/card/frame.png')));
47
+ assert.equal(result.libraryRoot, await fs.realpath(roots[entry.winner]));
48
+ }
49
+ });
50
+
51
+ test('env/location/legacy resolution has fixed write, read, and source expectations', async t => {
52
+ const temp = await fs.mkdtemp(path.join(tmpdir(), 'library-location-'));
53
+ t.after(() => fs.rm(temp, { recursive: true, force: true }));
54
+ const home = path.join(temp, 'home');
55
+ const legacy = path.join(home, 'assets');
56
+ const library = path.join(temp, 'library');
57
+ await fs.mkdir(home);
58
+ for (const value of [null, '{', {}, { version: 99, root: library, state: 'done' },
59
+ ...['pending', 'migrating', 'done', 'declined'].map(state => ({ version: 0, root: library, state }))]) {
60
+ const file = path.join(home, 'library-location.json');
61
+ if (value === null) await fs.rm(file, { force: true });
62
+ else await fs.writeFile(file, typeof value === 'string' ? value : JSON.stringify(value));
63
+ for (const override of [undefined, path.join(temp, 'override'), legacy]) {
64
+ const env = { AKARI_HOME: home, AKARI_LIBRARY_ROOT: override };
65
+ const active = value && value.version === 0 && ['migrating', 'done'].includes(value.state);
66
+ const expectedWrite = override || (active ? library : legacy);
67
+ assert.deepEqual(resolveAssetLibraryRoots(env), {
68
+ write: expectedWrite,
69
+ read: expectedWrite === legacy ? [legacy] : [expectedWrite, legacy],
70
+ source: override ? 'env' : active ? 'location' : 'legacy',
71
+ });
72
+ assert.equal(resolveAkariAssetsDir(env), expectedWrite);
73
+ }
74
+ }
75
+ assert.deepEqual(resolveAssetLibraryRoots({ HOME: temp }), {
76
+ write: path.join(temp, '.akari/assets'), read: [path.join(temp, '.akari/assets')], source: 'legacy',
77
+ });
78
+ for (const env of [{ USERPROFILE: temp }, { HOMEDRIVE: temp, HOMEPATH: '/profile' }]) {
79
+ const homeDir = env.USERPROFILE || `${env.HOMEDRIVE}${env.HOMEPATH}`;
80
+ const expected = path.join(homeDir, '.akari/assets');
81
+ assert.deepEqual(resolveAssetLibraryRoots(env, { platform: 'win32' }), {
82
+ write: expected, read: [expected], source: 'legacy',
83
+ });
84
+ }
85
+ });
86
+
87
+ for (const previousRootPresent of [false, true]) {
88
+ for (const state of ['pending', 'migrating', 'done', 'declined']) {
89
+ test(`previousRoot ${previousRootPresent ? 'present' : 'absent'} / ${state}: write, read, source`, async t => {
90
+ const temp = await fs.mkdtemp(path.join(tmpdir(), 'library-previous-'));
91
+ t.after(() => fs.rm(temp, { recursive: true, force: true }));
92
+ const home = path.join(temp, 'home');
93
+ const legacy = path.join(home, 'assets');
94
+ const root = path.join(temp, 'next');
95
+ const previousRoot = path.join(temp, 'previous');
96
+ await fs.mkdir(home);
97
+ await fs.writeFile(path.join(home, 'library-location.json'), JSON.stringify({
98
+ version: 0, root, state, ...(previousRootPresent ? { previousRoot } : {}),
99
+ }));
100
+ const env = { AKARI_HOME: home };
101
+ const migrated = state === 'migrating' || state === 'done';
102
+ const write = migrated ? root : previousRootPresent ? previousRoot : legacy;
103
+ const read = migrated
104
+ ? previousRootPresent ? [root, previousRoot, legacy] : [root, legacy]
105
+ : previousRootPresent ? [previousRoot, legacy] : [legacy];
106
+ const source = migrated || previousRootPresent ? 'location' : 'legacy';
107
+ assert.deepEqual(resolveAssetLibraryRoots(env), { write, read, source });
108
+ assert.equal(resolveAkariAssetsDir(env), write);
109
+ const override = path.join(temp, 'override');
110
+ assert.deepEqual(resolveAssetLibraryRoots({ ...env, AKARI_LIBRARY_ROOT: override }), {
111
+ write: override,
112
+ read: previousRootPresent ? [override, previousRoot, legacy] : [override, legacy],
113
+ source: 'env',
114
+ });
115
+ });
116
+ }
117
+ }
118
+
119
+ test('read roots deduplicate by realpath, including the temp directory alias', async t => {
120
+ const temp = await fs.mkdtemp(path.join(tmpdir(), 'library-realpath-'));
121
+ t.after(() => fs.rm(temp, { recursive: true, force: true }));
122
+ const home = path.join(temp, 'home');
123
+ const legacy = path.join(home, 'assets');
124
+ const alias = path.join(temp, 'alias');
125
+ await fs.mkdir(legacy, { recursive: true });
126
+ await fs.symlink(legacy, alias, 'dir');
127
+ await fs.writeFile(path.join(home, 'library-location.json'), JSON.stringify({
128
+ version: 0, root: path.join(temp, 'next'), state: 'migrating', previousRoot: alias,
129
+ }));
130
+ assert.deepEqual(resolveAssetLibraryRoots({ AKARI_HOME: home }).read, [path.join(temp, 'next'), alias]);
131
+ });
132
+
133
+ test('default library roots find an asset under previousRoot', async t => {
134
+ const temp = await fs.mkdtemp(path.join(tmpdir(), 'library-previous-fallback-'));
135
+ t.after(() => fs.rm(temp, { recursive: true, force: true }));
136
+ const home = path.join(temp, 'home');
137
+ const previousRoot = path.join(temp, 'previous');
138
+ const file = path.join(previousRoot, 'still/card/frame.png');
139
+ await fs.mkdir(home);
140
+ await fs.mkdir(path.dirname(file), { recursive: true });
141
+ await fs.writeFile(file, 'previous asset');
142
+ await fs.writeFile(path.join(home, 'library-location.json'), JSON.stringify({
143
+ version: 0, root: path.join(temp, 'next'), state: 'migrating', previousRoot,
144
+ }));
145
+ const oldHome = process.env.AKARI_HOME;
146
+ const oldOverride = process.env.AKARI_LIBRARY_ROOT;
147
+ process.env.AKARI_HOME = home;
148
+ delete process.env.AKARI_LIBRARY_ROOT;
149
+ t.after(() => {
150
+ if (oldHome === undefined) delete process.env.AKARI_HOME;
151
+ else process.env.AKARI_HOME = oldHome;
152
+ if (oldOverride === undefined) delete process.env.AKARI_LIBRARY_ROOT;
153
+ else process.env.AKARI_LIBRARY_ROOT = oldOverride;
154
+ });
155
+ const result = resolveLibraryFallback({
156
+ projectRoot: path.join(temp, 'project'),
157
+ declaredPath: 'assets/still/card/frame.png',
158
+ references: [{ category: 'still', id: 'card' }],
159
+ });
160
+ assert.deepEqual(result, {
161
+ matched: true, path: await fs.realpath(file), libraryRoot: await fs.realpath(previousRoot),
162
+ });
163
+ });
@@ -18,6 +18,7 @@ import { renderLintReport } from "./report.mjs";
18
18
  import { isInlineOverlayHtml, isSourceCompatibleWithLane } from "./shape-lane.mjs";
19
19
  import { collectLicenseFindings } from "./license-findings.mjs";
20
20
  import { describeFragmentAssetHint, extractFragmentAssetReferences, extractAbsoluteFragmentAssetReferences } from "../../render-cut/src/fragment-assets.mjs";
21
+ import { htmlTags, rawTextElements, stripHtmlComments } from "../../render-cut/src/html-scan.mjs";
21
22
  import { deriveTracks } from "./derive-tracks.mjs";
22
23
  import { segmentDuration } from "./cut-timeline.mjs";
23
24
  import { musicGrid } from "../../audio-library-setup/shared/beat-grid.mjs";
@@ -2660,6 +2661,7 @@ async function validateOverlays(overlays, timeline, findings, paths) {
2660
2661
  findings,
2661
2662
  );
2662
2663
  for (const finding of validateWorldSceneDeclaration(html, await readFile(join(paths.projectRoot, "planning/world-map.json"), "utf8").catch(error => error?.code === "ENOENT" ? null : Promise.reject(error)), isHtmlFile ? relativePath(paths.projectRoot, htmlPath) : `${itemPath}.html`)) addFinding(findings, finding);
2664
+ validateThreeCanvas(html, isHtmlFile ? relativePath(paths.projectRoot, htmlPath) : `${itemPath}.html`, findings);
2663
2665
  if (!isHtmlFile) continue;
2664
2666
 
2665
2667
  validateOverlayFragmentAssets(html, overlay, paths, findings);
@@ -2732,6 +2734,48 @@ async function validateOverlays(overlays, timeline, findings, paths) {
2732
2734
  }
2733
2735
  }
2734
2736
 
2737
+ function validateThreeCanvas(html, path, findings) {
2738
+ const source = stripHtmlComments(html);
2739
+ const raw = [
2740
+ ...rawTextElements(source, "script"),
2741
+ ...rawTextElements(source, "style"),
2742
+ ].sort((left, right) => left.start - right.start);
2743
+ let rawIndex = 0;
2744
+ let scene = false;
2745
+ let marked = 0;
2746
+ let invalidMarked = false;
2747
+ let canvases = 0;
2748
+ for (const tag of htmlTags(source)) {
2749
+ while (rawIndex < raw.length && tag.start >= raw[rawIndex].end) rawIndex += 1;
2750
+ const block = raw[rawIndex];
2751
+ if (block && tag.start > block.start && tag.start < block.end) continue;
2752
+ const match = /^<([a-z][\w:-]*)\b/iu.exec(tag.text);
2753
+ if (!match) continue;
2754
+ const name = match[1].toLowerCase();
2755
+ const attributes = parseHtmlAttributes(tag.text);
2756
+ if (name === "script" && Object.hasOwn(attributes, "data-akari-3d-scene")
2757
+ && attributes.type?.toLowerCase() === "application/json") scene = true;
2758
+ if (name === "canvas") canvases += 1;
2759
+ if (Object.hasOwn(attributes, "data-akari-3d-canvas")) {
2760
+ marked += 1;
2761
+ if (name !== "canvas") invalidMarked = true;
2762
+ }
2763
+ }
2764
+ if (!scene) return;
2765
+ if (marked > 1) addFinding(findings, {
2766
+ severity: "error", check: "overlays.three-canvas",
2767
+ message: `data-akari-3d-canvas must appear on at most one element (found ${marked}).`, path,
2768
+ });
2769
+ if (invalidMarked) addFinding(findings, {
2770
+ severity: "error", check: "overlays.three-canvas",
2771
+ message: "data-akari-3d-canvas must be attached to a <canvas> element.", path,
2772
+ });
2773
+ if (marked === 0 && canvases > 1) addFinding(findings, {
2774
+ severity: "warning", check: "overlays.three-canvas",
2775
+ message: "3D renders to the first canvas in document order; add data-akari-3d-canvas to the intended canvas.", path,
2776
+ });
2777
+ }
2778
+
2735
2779
  function validateOverlayFragmentAssets(html, overlay, paths, findings) {
2736
2780
  if (overlay.html.trimStart().startsWith("<")) return;
2737
2781
  const root = realpathSync(paths.projectRoot);
@@ -1,97 +1 @@
1
- import { lstatSync, readFileSync, realpathSync } from "node:fs";
2
- import os from "node:os";
3
- import { isAbsolute, join, relative, resolve } from "node:path";
4
-
5
- // 参照台帳(.akari/asset-references.json)のスキーマ版数。edit.json の version とは無関係。
6
- const REFERENCES_SCHEMA_VERSION = 0;
7
-
8
- export function readProjectReferences(projectRoot) {
9
- try {
10
- const parsed = JSON.parse(readFileSync(
11
- join(projectRoot, ".akari", "asset-references.json"),
12
- "utf8",
13
- ));
14
- if (parsed?.version !== REFERENCES_SCHEMA_VERSION || !Array.isArray(parsed.references)) return [];
15
- return parsed.references.filter((entry) => entry !== null
16
- && typeof entry === "object"
17
- && !Array.isArray(entry)
18
- && typeof entry.id === "string"
19
- && entry.id.length > 0
20
- && typeof entry.category === "string"
21
- && entry.category.length > 0)
22
- .map((entry) => ({ id: entry.id, category: entry.category }));
23
- } catch {
24
- return [];
25
- }
26
- }
27
-
28
- // Mirrored from creator-root; keep both consumers covered by the same case table.
29
- const LIBRARY_LOCATION_VERSION = 0;
30
- export function resolveAssetLibraryRoots(env = process.env, { platform = process.platform } = {}) {
31
- const homeDir = platform === 'win32'
32
- ? env.USERPROFILE || (env.HOMEDRIVE && env.HOMEPATH ? `${env.HOMEDRIVE}${env.HOMEPATH}` : os.homedir())
33
- : env.HOME || os.homedir();
34
- const home = env.AKARI_HOME || join(homeDir, '.akari');
35
- const legacy = resolve(home, 'assets');
36
- let location;
37
- try {
38
- const parsed = JSON.parse(readFileSync(join(home, 'library-location.json'), 'utf8'));
39
- if (parsed?.version === LIBRARY_LOCATION_VERSION && typeof parsed.root === 'string'
40
- && isAbsolute(parsed.root) && ['pending', 'migrating', 'done', 'declined'].includes(parsed.state)) location = parsed;
41
- } catch { /* undecided */ }
42
- const write = env.AKARI_LIBRARY_ROOT ? resolve(env.AKARI_LIBRARY_ROOT)
43
- : location && ['migrating', 'done'].includes(location.state) ? resolve(location.root) : legacy;
44
- return { write, read: [...new Set([write, legacy])],
45
- source: env.AKARI_LIBRARY_ROOT ? 'env' : location && ['migrating', 'done'].includes(location.state) ? 'location' : 'legacy' };
46
- }
47
- export function resolveAkariAssetsDir(env = process.env) {
48
- return resolveAssetLibraryRoots(env).write;
49
- }
50
-
51
- function declaredAssetParts(projectRoot, declaredPath) {
52
- if (typeof declaredPath !== "string" || declaredPath.length === 0) return null;
53
- const rawSegments = declaredPath.replaceAll("\\", "/").split("/");
54
- if (rawSegments.some((segment) => segment === "." || segment === "..")) return null;
55
- const root = resolve(projectRoot);
56
- const lexical = isAbsolute(declaredPath) ? resolve(declaredPath) : resolve(root, declaredPath);
57
- if (!isWithin(root, lexical)) return null;
58
- const projectRelative = relative(root, lexical).replaceAll("\\", "/");
59
- const segments = projectRelative.split("/");
60
- if (segments.length < 4
61
- || segments[0] !== "assets"
62
- || segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
63
- return null;
64
- }
65
- return { category: segments[1], id: segments[2], rest: segments.slice(3) };
66
- }
67
-
68
- export function resolveLibraryFallback({
69
- projectRoot,
70
- declaredPath,
71
- references = readProjectReferences(projectRoot),
72
- akariAssetsDir,
73
- libraryRoots = akariAssetsDir ? [akariAssetsDir] : resolveAssetLibraryRoots().read,
74
- }) {
75
- const parts = declaredAssetParts(projectRoot, declaredPath);
76
- if (!parts || !references.some(
77
- (entry) => entry.category === parts.category && entry.id === parts.id,
78
- )) return { matched: false, path: null, libraryRoot: null };
79
-
80
- for (const root of libraryRoots) {
81
- const lexicalRoot = resolve(root);
82
- const lexicalTarget = resolve(lexicalRoot, parts.category, parts.id, ...parts.rest);
83
- if (!isWithin(lexicalRoot, lexicalTarget)) continue;
84
- try {
85
- const actualRoot = realpathSync(lexicalRoot);
86
- const actualTarget = realpathSync(lexicalTarget);
87
- if (!isWithin(actualRoot, actualTarget) || !lstatSync(actualTarget).isFile()) continue;
88
- return { matched: true, path: actualTarget, libraryRoot: actualRoot };
89
- } catch { /* Try the previous location during migration. */ }
90
- }
91
- return { matched: true, path: null, libraryRoot: null };
92
- }
93
-
94
- function isWithin(root, target) {
95
- const value = relative(root, target);
96
- return value === "" || (!value.startsWith("..") && !isAbsolute(value));
97
- }
1
+ export * from "../../creator-root/src/library-reference.mjs";
@@ -1,41 +1 @@
1
- const ALLOWED_COMMERCIAL = new Set(['allowed', 'prohibited', 'unknown']);
2
- const KNOWN_FREE = new Set([
3
- 'CC0-1.0', 'LicenseRef-AKARI-Assets-v0', 'LicenseRef-AKARI-Sounds-Terms-v0',
4
- 'MIT', 'OFL-1.1',
5
- ]);
6
-
7
- /** Derive the two independent questions without rewriting older metadata. */
8
- export function deriveLicenseAxes(license) {
9
- if (!license || typeof license !== 'object') {
10
- return { commercial: 'unknown', attributionRequired: null };
11
- }
12
- const spdx = typeof license.spdx === 'string' ? license.spdx.trim() : '';
13
- const scope = typeof license.scope === 'string' ? license.scope.trim().toLowerCase() : '';
14
- let commercial = 'unknown';
15
- let attributionRequired = null;
16
-
17
- if (KNOWN_FREE.has(spdx)) {
18
- commercial = 'allowed';
19
- attributionRequired = false;
20
- } else if (/^CC-BY(?:-[A-Z]+)*-\d+(?:\.\d+)?$/i.test(spdx)) {
21
- commercial = /(?:^|-)NC(?:-|$)/i.test(spdx) ? 'prohibited' : 'allowed';
22
- attributionRequired = true;
23
- } else if (/^CC-BY-NC(?:-|$)/i.test(spdx)) {
24
- commercial = 'prohibited';
25
- attributionRequired = true;
26
- }
27
-
28
- if (scope === 'commercial-ok' || scope === 'attribution') commercial = 'allowed';
29
- if (scope === 'non-commercial') commercial = 'prohibited';
30
- if (/(?:^|-)NC(?:-|$)/i.test(spdx)) commercial = 'prohibited';
31
- if (scope === 'attribution') attributionRequired = true;
32
- if (typeof license.attribution_required === 'boolean') {
33
- attributionRequired = license.attribution_required;
34
- }
35
- if (/^CC-BY(?:-[A-Z]+)*-\d+(?:\.\d+)?$/i.test(spdx)) attributionRequired = true;
36
- if (ALLOWED_COMMERCIAL.has(license.commercial)) commercial = license.commercial;
37
- if (typeof license.attributionRequired === 'boolean' || license.attributionRequired === null) {
38
- attributionRequired = license.attributionRequired;
39
- }
40
- return { commercial, attributionRequired };
41
- }
1
+ export * from "../../asset-resolver/src/license-axes.mjs";
@@ -399,9 +399,9 @@ function resolveProjectedWordStyles(captions, projectedCaptions, emphasisValue,
399
399
  || !finiteNonNegative(value.start) || !finiteNonNegative(value.end)
400
400
  || value.start !== value.end)
401
401
  return [];
402
- const later = caption.words.slice(wordIndex + 1).find(candidate => projected.words.includes(candidate));
402
+ const later = caption.words.slice(wordIndex + 1).find((candidate) => projected.words.includes(candidate));
403
403
  const earlier = caption.words.slice(0, wordIndex).reverse()
404
- .find(candidate => projected.words.includes(candidate));
404
+ .find((candidate) => projected.words.includes(candidate));
405
405
  const anchor = later ?? earlier ?? projected.words[0];
406
406
  return [{ word: { text: value.text, start: anchor.start, end: anchor.end }, synthetic: true }];
407
407
  });
@@ -0,0 +1,2 @@
1
+ /** Shared specimen for caption, My Style, and text style library cards. */
2
+ export declare const CAPTION_SAMPLE_TEXT = "\u3042\u30A212";
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CAPTION_SAMPLE_TEXT = void 0;
4
+ /** Shared specimen for caption, My Style, and text style library cards. */
5
+ exports.CAPTION_SAMPLE_TEXT = 'あア12';
@@ -5,6 +5,7 @@
5
5
  */
6
6
  export * from './edit-store';
7
7
  export * from './caption-store';
8
+ export * from './caption-sample-text';
8
9
  export * from './caption-style-preset';
9
10
  export * from './generated/textstyle-catalog';
10
11
  export * from './caption-words-rederive';
@@ -28,6 +28,7 @@ exports.withNewerVersionLintPrefix = withNewerVersionLintPrefix;
28
28
  */
29
29
  __exportStar(require("./edit-store"), exports);
30
30
  __exportStar(require("./caption-store"), exports);
31
+ __exportStar(require("./caption-sample-text"), exports);
31
32
  __exportStar(require("./caption-style-preset"), exports);
32
33
  __exportStar(require("./generated/textstyle-catalog"), exports);
33
34
  __exportStar(require("./caption-words-rederive"), exports);
@@ -49,7 +49,7 @@ function resolveDeclaredCaptionTrackZ(tracks) {
49
49
  for (let z = 0; z < declared.length; z += 1) {
50
50
  const track = declared[z];
51
51
  const hasCaptionBag = (Array.isArray(track?.items) ? track.items : [])
52
- .some(item => item?.source?.kind === 'captions');
52
+ .some((item) => item?.source?.kind === 'captions');
53
53
  if (hasCaptionBag || track?.content?.from === 'captions.json' || track?.legacy?.kind === 'captions')
54
54
  return z;
55
55
  }
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  var AkariEditKernel = (() => {
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -19,7 +19,7 @@
19
19
  // 失敗した場合は実際の不具合なのでエラーを投げる。
20
20
 
21
21
  import { createHash } from "node:crypto";
22
- import { createReadStream, createWriteStream, existsSync } from "node:fs";
22
+ import { createReadStream, createWriteStream, existsSync, realpathSync } from "node:fs";
23
23
  import { chmod, copyFile, mkdir, readdir, rm, stat } from "node:fs/promises";
24
24
  import https from "node:https";
25
25
  import os from "node:os";
@@ -206,7 +206,17 @@ export async function buildWhisperCli({ target = currentTarget(), force = false,
206
206
  };
207
207
  }
208
208
 
209
- const isMainModule = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
209
+ // 両辺 realpath(AGENTS.md の規約)。失敗時は false = 実行しない(fail-closed)
210
+ function isDirectRun() {
211
+ if (!process.argv[1]) return false;
212
+ try {
213
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
214
+ } catch {
215
+ return false;
216
+ }
217
+ }
218
+
219
+ const isMainModule = isDirectRun();
210
220
  if (isMainModule) {
211
221
  const forceFlag = process.argv.includes("--force");
212
222
  buildWhisperCli({ force: forceFlag, log: (msg) => console.log(msg) })