akari-video 0.1.13 → 0.1.14
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/bin/akari.mjs +8 -3
- package/package.json +1 -1
- package/src/cli.mjs +12 -7
- package/src/messages.mjs +26 -2
- package/src/update-check.mjs +66 -25
- package/vendor/.akari-capability-sources.json +1 -0
- package/vendor/docs/contract-2026-07-25-r6-audio-tracks-and-trim.md +1 -1
- package/vendor/docs/contract-2026-08-23-stroke-persistence.md +75 -0
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/decision-cards/README.md +12 -0
- package/vendor/packages/decision-cards/package.json +3 -2
- package/vendor/packages/edit-lint/src/edit-lint.mjs +41 -60
- package/vendor/packages/edit-store/lib/cut-adjacency.d.ts +19 -0
- package/vendor/packages/edit-store/lib/cut-adjacency.js +28 -0
- package/vendor/packages/edit-store/lib/edit-v2-item-write.d.ts +53 -0
- package/vendor/packages/edit-store/lib/edit-v2-item-write.js +162 -0
- package/vendor/packages/edit-store/lib/index.d.ts +3 -0
- package/vendor/packages/edit-store/lib/index.js +3 -0
- package/vendor/packages/edit-store/lib/migrate/index.js +1 -1
- package/vendor/packages/edit-store/lib/track-transition-compatibility.d.ts +23 -0
- package/vendor/packages/edit-store/lib/track-transition-compatibility.js +76 -0
- package/vendor/packages/edit-store/lib/write-gate.d.ts +2 -0
- package/vendor/packages/edit-store/lib/write-gate.js +11 -1
- package/vendor/packages/overlay-runtime/package.json +2 -1
- package/vendor/packages/pen-visuals/package.json +1 -0
- package/vendor/packages/schemas/bin/validate-research-plan.mjs +55 -7
- package/vendor/packages/schemas/fixtures/review/valid-strokes/review.json +1 -1
- package/vendor/packages/schemas/research-plan.schema.json +24 -2
- package/vendor/packages/schemas/review.schema.json +18 -0
- package/vendor/packages/schemas/test/fixtures/research-plan/invalid-nested-cutaway/research-plan.json +25 -0
- package/vendor/packages/schemas/test/fixtures/research-plan/valid-legacy-without-shot-ids/research-plan.json +29 -0
- package/vendor/packages/schemas/test/fixtures/research-plan/valid-visual-storyboard/research-plan.json +72 -0
- package/vendor/packages/schemas/test/validate-research-plan.test.mjs +18 -0
- package/vendor/skills/address-review/bin/list.mjs +12 -0
- package/vendor/skills/address-review/dev-fixtures/fixture-project/review.json +3 -0
- package/vendor/skills/address-review/test/review-store.test.mjs +6 -0
- package/vendor/skills/compile-review-session/bin/core/compiler.mjs +5 -0
- package/vendor/skills/compile-review-session/test/compiler.test.mjs +9 -0
- package/vendor/skills/compile-review-session/test/ui-events.test.mjs +1 -0
- package/vendor/skills/edit-plan/beat-sync.md +17 -3
- package/vendor/skills/research-plan/SKILL.md +1 -1
- package/vendor/skills/research-plan/storyboard.md +30 -1
package/bin/akari.mjs
CHANGED
|
@@ -11,14 +11,19 @@ import { runCapabilityCommand } from '../src/capability-command.mjs';
|
|
|
11
11
|
import { runStoreCommand } from '../src/store-command.mjs';
|
|
12
12
|
import { runAssetsCommand } from '../src/assets-command.mjs';
|
|
13
13
|
import { runMigrateCommand } from '../src/migrate-command.mjs';
|
|
14
|
-
import { maybeApplyPendingUpdateOnLaunch,
|
|
15
|
-
import { describeCliHelp } from '../src/messages.mjs';
|
|
14
|
+
import { maybeApplyPendingUpdateOnLaunch, resolveInstalledVersionInfo } from '../src/update-check.mjs';
|
|
15
|
+
import { describeCliHelp, describeInstalledVersions } from '../src/messages.mjs';
|
|
16
16
|
|
|
17
17
|
// `akari --version` / `-v`: インストール済みの版を表示するだけの最小コマンド
|
|
18
18
|
// (タスク契約 2026-08-11-update-u4-cli-self-update の受け入れ条件 —
|
|
19
19
|
// `akari update` / `--rollback` 後にインストール先の版を観測する手段として必要)。
|
|
20
20
|
async function printVersion() {
|
|
21
|
-
|
|
21
|
+
const versionInfo = resolveInstalledVersionInfo({ env: process.env });
|
|
22
|
+
// 1 行目は update / rollback の既存機械観測契約として CLI 版だけを維持する。
|
|
23
|
+
console.log(`v${versionInfo.cliVersion}`);
|
|
24
|
+
for (const line of describeInstalledVersions(versionInfo)) {
|
|
25
|
+
console.log(line);
|
|
26
|
+
}
|
|
22
27
|
return { exitCode: 0 };
|
|
23
28
|
}
|
|
24
29
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
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/cli.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { resolveLauncherAssets } from './repo-assets.mjs';
|
|
|
6
6
|
import { detectProjectState } from './project-state.mjs';
|
|
7
7
|
import { findClaudeExecutable, findOpencodeExecutable } from './path-lookup.mjs';
|
|
8
8
|
import { loadTaskLabels } from './task-labels.mjs';
|
|
9
|
-
import { describeInstalledVersions, describeIntake, claudeMissingGuidance, opencodeMissingGuidance, describeUpdateCommand, describeVersionStatus, formatUpdateNotice } from './messages.mjs';
|
|
9
|
+
import { describeForceReinstall, describeInstalledVersions, describeIntake, claudeMissingGuidance, opencodeMissingGuidance, describeUpdateCommand, describeVersionStatus, formatUpdateNotice } from './messages.mjs';
|
|
10
10
|
import { resolveEffectiveProjectRoot } from './first-run.mjs';
|
|
11
11
|
import { maybeShowAssetIntroNotice } from './sounds-setup.mjs';
|
|
12
12
|
import {
|
|
@@ -14,9 +14,9 @@ import {
|
|
|
14
14
|
compareVersions,
|
|
15
15
|
isValidFeedShape,
|
|
16
16
|
readCacheSync,
|
|
17
|
-
readInstalledAppVersion,
|
|
18
17
|
readOwnVersion,
|
|
19
18
|
recordDismissalSync,
|
|
19
|
+
refreshUpdateFeed,
|
|
20
20
|
resolveCachePath,
|
|
21
21
|
resolveInstalledVersionInfo,
|
|
22
22
|
triggerBackgroundRefresh
|
|
@@ -188,8 +188,8 @@ function defaultSpawnOpencode(opencodePath, args, projectRoot) {
|
|
|
188
188
|
* 自己更新を試みず、キャッシュに載っている最新版の通知を今後出さないよう記録するだけ
|
|
189
189
|
* (既存挙動を維持)。`--force` は同じ版の本体も再導入し、`--rollback` は直前 1 世代
|
|
190
190
|
* (`~/.akari/app-previous/`)へ戻す。
|
|
191
|
-
*
|
|
192
|
-
*
|
|
191
|
+
* 通常時にネットワークへ触れるのは自己更新の DL 区間のみ。`--force` だけはキャッシュが
|
|
192
|
+
* 未取得なら、復旧経路を塞がないためフィードの同期取得を 1 回試す。
|
|
193
193
|
*/
|
|
194
194
|
export async function runUpdateCommand(args, options = {}) {
|
|
195
195
|
const log = options.log ?? ((line) => console.log(line));
|
|
@@ -197,7 +197,7 @@ export async function runUpdateCommand(args, options = {}) {
|
|
|
197
197
|
const versionInfo = resolveCommandVersionInfo(options, env);
|
|
198
198
|
const currentVersion = versionInfo.currentVersion;
|
|
199
199
|
const cachePath = resolveCachePath(env);
|
|
200
|
-
|
|
200
|
+
let cache = readCacheSync(cachePath);
|
|
201
201
|
const dismissRequested = args.includes('--dismiss');
|
|
202
202
|
const rollbackRequested = args.includes('--rollback');
|
|
203
203
|
const forceRequested = args.includes('--force');
|
|
@@ -219,10 +219,15 @@ export async function runUpdateCommand(args, options = {}) {
|
|
|
219
219
|
return { exitCode: 0 };
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
if (forceRequested && !cache?.feed) {
|
|
223
|
+
await (options.refreshUpdateFeed ?? refreshUpdateFeed)({ env, fetchImpl: options.fetchImpl });
|
|
224
|
+
cache = readCacheSync(cachePath);
|
|
225
|
+
}
|
|
226
|
+
|
|
222
227
|
const feed = cache?.feed;
|
|
223
228
|
const updateAvailable = isValidFeedShape(feed) && compareVersions(feed.product, currentVersion) > 0;
|
|
224
229
|
const reinstallRequested = forceRequested && isValidFeedShape(feed) && compareVersions(feed.product, currentVersion) >= 0;
|
|
225
|
-
const hasManagedApp =
|
|
230
|
+
const hasManagedApp = versionInfo.managedApp === true;
|
|
226
231
|
const selfUpdateEligible = (updateAvailable || reinstallRequested)
|
|
227
232
|
&& !!feed.components?.app?.url
|
|
228
233
|
&& !!feed.components?.app?.sha256
|
|
@@ -240,7 +245,7 @@ export async function runUpdateCommand(args, options = {}) {
|
|
|
240
245
|
}
|
|
241
246
|
log(`最新バージョン: v${feed.product}`);
|
|
242
247
|
if (forceRequested) {
|
|
243
|
-
log(
|
|
248
|
+
log(describeForceReinstall(versionInfo, feed.product));
|
|
244
249
|
}
|
|
245
250
|
|
|
246
251
|
return (options.applySelfUpdate ?? applySelfUpdate)({
|
package/src/messages.mjs
CHANGED
|
@@ -90,6 +90,14 @@ export function describeVersionStatus(versionOrInfo, cache) {
|
|
|
90
90
|
|
|
91
91
|
export function describeInstalledVersions(versionOrInfo) {
|
|
92
92
|
const info = normalizeVersionInfo(versionOrInfo);
|
|
93
|
+
if (info.installRefNeedsRepair) {
|
|
94
|
+
const installRefPath = info.installRefPath ?? '~/.akari/app/.akari-install-ref';
|
|
95
|
+
return [
|
|
96
|
+
`CLI バージョン: v${info.cliVersion}`,
|
|
97
|
+
`本体版を判定できません(\`${installRefPath}\` が壊れています)。`,
|
|
98
|
+
'修復するには `akari update --force` を実行してください。'
|
|
99
|
+
];
|
|
100
|
+
}
|
|
93
101
|
if (!info.appVersion) {
|
|
94
102
|
return [
|
|
95
103
|
`現在のバージョン: v${info.currentVersion}`,
|
|
@@ -104,6 +112,13 @@ export function describeInstalledVersions(versionOrInfo) {
|
|
|
104
112
|
return lines;
|
|
105
113
|
}
|
|
106
114
|
|
|
115
|
+
export function describeForceReinstall(versionOrInfo, targetVersion) {
|
|
116
|
+
const info = normalizeVersionInfo(versionOrInfo);
|
|
117
|
+
return info.installRefNeedsRepair
|
|
118
|
+
? `--force: 版を判定できない本体 → v${targetVersion} を入れ直します。`
|
|
119
|
+
: `--force: 本体 v${info.currentVersion} → v${targetVersion} を入れ直します。`;
|
|
120
|
+
}
|
|
121
|
+
|
|
107
122
|
function normalizeVersionInfo(value) {
|
|
108
123
|
if (typeof value === 'string') {
|
|
109
124
|
return { cliVersion: value, appVersion: null, currentVersion: value, mismatch: false };
|
|
@@ -112,11 +127,15 @@ function normalizeVersionInfo(value) {
|
|
|
112
127
|
}
|
|
113
128
|
|
|
114
129
|
function versionRelationLabel(info) {
|
|
115
|
-
return compareVersions(info.appVersion, info.cliVersion) < 0 ? '本体が古い' : 'CLI
|
|
130
|
+
return compareVersions(info.appVersion, info.cliVersion) < 0 ? '本体が古い' : 'CLI が古い';
|
|
116
131
|
}
|
|
117
132
|
|
|
118
133
|
function formatVersionMismatch(info) {
|
|
119
|
-
|
|
134
|
+
const relation = versionRelationLabel(info);
|
|
135
|
+
const guidance = relation === '本体が古い'
|
|
136
|
+
? '`akari update` で本体を更新してください。'
|
|
137
|
+
: '`npm i -g akari-video@latest` で CLI を更新してください。';
|
|
138
|
+
return `⚠ CLI v${info.cliVersion} / 本体 v${info.appVersion} → ${relation}。${guidance}`;
|
|
120
139
|
}
|
|
121
140
|
|
|
122
141
|
/**
|
|
@@ -168,6 +187,11 @@ export function describeUpdateCommand({ currentVersion, versionInfo, cache, dism
|
|
|
168
187
|
lines.push(`リリースノート: ${feed.notes_url}`);
|
|
169
188
|
}
|
|
170
189
|
|
|
190
|
+
if (info.installRefNeedsRepair) {
|
|
191
|
+
lines.push('本体版を判定できないため、更新判定を行いません。');
|
|
192
|
+
return lines;
|
|
193
|
+
}
|
|
194
|
+
|
|
171
195
|
if (compareVersions(feed.product, info.currentVersion) <= 0) {
|
|
172
196
|
lines.push('お使いのバージョンは最新です。');
|
|
173
197
|
return lines;
|
package/src/update-check.mjs
CHANGED
|
@@ -65,29 +65,49 @@ export function readOwnVersion() {
|
|
|
65
65
|
return JSON.parse(raw).version;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/** install-ref を「有効・未記録・破損」の 3 状態で読む内部表現。 */
|
|
69
|
+
function readInstalledAppVersionInfo(env = process.env) {
|
|
70
|
+
const path = resolveInstallRefPath(env);
|
|
71
|
+
try {
|
|
72
|
+
const raw = readFileSync(path, 'utf8').trim();
|
|
73
|
+
const match = raw.match(/^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/);
|
|
74
|
+
return match
|
|
75
|
+
? { status: 'valid', version: match[1], path }
|
|
76
|
+
: { status: 'invalid', version: null, path };
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return {
|
|
79
|
+
status: error?.code === 'ENOENT' ? 'missing' : 'invalid',
|
|
80
|
+
version: null,
|
|
81
|
+
path
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
68
86
|
/**
|
|
69
87
|
* 実際に render-cut / edit-lint を実行する本体の導入版。
|
|
70
|
-
* install.sh / self-update.mjs が書く `vX.Y.Z`
|
|
88
|
+
* install.sh / self-update.mjs が書く `vX.Y.Z` を読む。互換 API として版または null を返し、
|
|
89
|
+
* 未記録と破損の区別は `resolveInstalledVersionInfo()` が保持する。
|
|
71
90
|
*/
|
|
72
91
|
export function readInstalledAppVersion(env = process.env) {
|
|
73
|
-
|
|
74
|
-
const raw = readFileSync(resolveInstallRefPath(env), 'utf8').trim();
|
|
75
|
-
const match = raw.match(/^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/);
|
|
76
|
-
return match?.[1] ?? null;
|
|
77
|
-
} catch {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
92
|
+
return readInstalledAppVersionInfo(env).version;
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
/** 更新判定に使う版と、CLI / 本体のずれを一度に解決する。 */
|
|
83
96
|
export function resolveInstalledVersionInfo({ env = process.env, cliVersion = readOwnVersion() } = {}) {
|
|
84
|
-
const
|
|
97
|
+
const installRef = readInstalledAppVersionInfo(env);
|
|
98
|
+
const installedAppVersion = installRef.version;
|
|
85
99
|
const currentVersion = installedAppVersion ?? cliVersion;
|
|
86
100
|
return {
|
|
87
101
|
cliVersion,
|
|
88
102
|
appVersion: installedAppVersion,
|
|
89
103
|
currentVersion,
|
|
90
|
-
source:
|
|
104
|
+
source: installRef.status === 'valid'
|
|
105
|
+
? 'install-ref'
|
|
106
|
+
: installRef.status === 'invalid' ? 'invalid-install-ref' : 'cli-fallback',
|
|
107
|
+
installRefStatus: installRef.status,
|
|
108
|
+
installRefPath: installRef.path,
|
|
109
|
+
installRefNeedsRepair: installRef.status === 'invalid',
|
|
110
|
+
managedApp: installRef.status !== 'missing',
|
|
91
111
|
mismatch: installedAppVersion !== null && compareVersions(cliVersion, installedAppVersion) !== 0
|
|
92
112
|
};
|
|
93
113
|
}
|
|
@@ -135,8 +155,18 @@ function writeCacheSync(cachePath, cache) {
|
|
|
135
155
|
/**
|
|
136
156
|
* キャッシュと現在版から、新版通知を出すべきかを判定する(同期・純粋関数・I/O なし)。
|
|
137
157
|
*/
|
|
138
|
-
export function evaluateUpdateStatus({ currentVersion, cache, cliVersion = currentVersion, appVersion = null, source = 'cli-fallback', mismatch = false }) {
|
|
139
|
-
const versionDetails = {
|
|
158
|
+
export function evaluateUpdateStatus({ currentVersion, cache, cliVersion = currentVersion, appVersion = null, source = 'cli-fallback', installRefStatus, installRefPath, installRefNeedsRepair, managedApp, mismatch = false }) {
|
|
159
|
+
const versionDetails = {
|
|
160
|
+
currentVersion,
|
|
161
|
+
cliVersion,
|
|
162
|
+
appVersion,
|
|
163
|
+
source,
|
|
164
|
+
installRefStatus,
|
|
165
|
+
installRefPath,
|
|
166
|
+
installRefNeedsRepair,
|
|
167
|
+
managedApp,
|
|
168
|
+
mismatch
|
|
169
|
+
};
|
|
140
170
|
const feed = cache?.feed;
|
|
141
171
|
if (!isValidFeedShape(feed)) {
|
|
142
172
|
return { available: false, ...versionDetails };
|
|
@@ -215,16 +245,8 @@ export async function maybeStageInBackground({ env = process.env, feed, fetchImp
|
|
|
215
245
|
return stageSelfUpdate({ env, feed, log: () => {}, fetchImpl, timeoutMs, extract });
|
|
216
246
|
}
|
|
217
247
|
|
|
218
|
-
/**
|
|
219
|
-
|
|
220
|
-
* すべて沈黙して return する(何も throw しない)。`dismissed` は既存キャッシュから
|
|
221
|
-
* 引き継ぐ(fetch のたびに既読状態が消えないように)。
|
|
222
|
-
*
|
|
223
|
-
* フィード取得・キャッシュ反映が成功した後、契約 §11 のバックグラウンド staging も
|
|
224
|
-
* 同じ沈黙原則のもとで試みる(`maybeStageInBackground`)。成功したときだけ
|
|
225
|
-
* キャッシュへ `staged`(版・sha256・staged_at)を追記する。
|
|
226
|
-
*/
|
|
227
|
-
export async function runBackgroundFetch({ env = process.env, fetchImpl = globalThis.fetch, launcherRoot } = {}) {
|
|
248
|
+
/** フィードを 1 回取得し、正常なら既読状態を保ったままキャッシュへ反映する。 */
|
|
249
|
+
export async function refreshUpdateFeed({ env = process.env, fetchImpl = globalThis.fetch } = {}) {
|
|
228
250
|
const feedUrl = resolveFeedUrl(env);
|
|
229
251
|
const cachePath = resolveCachePath(env);
|
|
230
252
|
try {
|
|
@@ -241,15 +263,34 @@ export async function runBackgroundFetch({ env = process.env, fetchImpl = global
|
|
|
241
263
|
}
|
|
242
264
|
const feed = await response.json();
|
|
243
265
|
if (!isValidFeedShape(feed)) {
|
|
244
|
-
return;
|
|
266
|
+
return null;
|
|
245
267
|
}
|
|
246
268
|
const existing = readCacheSync(cachePath);
|
|
247
|
-
|
|
269
|
+
const next = {
|
|
248
270
|
schema: CACHE_SCHEMA,
|
|
249
271
|
fetched_at: new Date().toISOString(),
|
|
250
272
|
feed,
|
|
251
273
|
dismissed: existing?.dismissed ?? {}
|
|
252
|
-
}
|
|
274
|
+
};
|
|
275
|
+
writeCacheSync(cachePath, next);
|
|
276
|
+
return next;
|
|
277
|
+
} catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* バックグラウンド fetch 本体。フィード取得失敗は沈黙し、成功後は契約 §11 の staging を
|
|
284
|
+
* 同じ沈黙原則で試す。成功したときだけキャッシュへ `staged` を追記する。
|
|
285
|
+
*/
|
|
286
|
+
export async function runBackgroundFetch({ env = process.env, fetchImpl = globalThis.fetch, launcherRoot } = {}) {
|
|
287
|
+
const cachePath = resolveCachePath(env);
|
|
288
|
+
try {
|
|
289
|
+
const refreshed = await refreshUpdateFeed({ env, fetchImpl });
|
|
290
|
+
const feed = refreshed?.feed;
|
|
291
|
+
if (!feed) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
253
294
|
|
|
254
295
|
const staged = await maybeStageInBackground({ env, feed, fetchImpl, launcherRoot });
|
|
255
296
|
if (staged?.ok) {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"docs/contract-2026-08-13-avatar-drive-v0.md",
|
|
41
41
|
"docs/contract-2026-08-14-avatar-vrm-v0.md",
|
|
42
42
|
"docs/contract-2026-08-18-v1-render-parity.md",
|
|
43
|
+
"docs/contract-2026-08-23-stroke-persistence.md",
|
|
43
44
|
"packages/akari-launcher/package.json",
|
|
44
45
|
"packages/akari-launcher/README.md",
|
|
45
46
|
"packages/akari-tools/package.json",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
|
|
67
67
|
## 5. §2 追記 — sfx フェード(audio-clip-fades, 2026-08-18・オーナー裁定「クリップ主義」T2)
|
|
68
68
|
|
|
69
|
-
BGM をクリップ化する裁定(内部リポ `
|
|
69
|
+
BGM をクリップ化する裁定(内部リポ `akari-video-internal` の該当タスク)に伴い、
|
|
70
70
|
「音楽をクリップ(audio.sfx[])として置いても BGM ベッドと同じフェード表現ができる」を
|
|
71
71
|
満たすため、`sfxItem` に optional の `fade_in` / `fade_out`(秒・0 以上)を追加のみ拡張する
|
|
72
72
|
(`version` 不変・`contract-2026-07-17-data-contract-versioning.md` の原則に従う)。
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
lifecycle: implemented
|
|
3
|
+
created: 2026-08-23
|
|
4
|
+
updated: 2026-08-23
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 注釈ストローク永続表示契約
|
|
8
|
+
|
|
9
|
+
- 日付: 2026-08-23
|
|
10
|
+
- 状態: **実装済み**
|
|
11
|
+
- 前提: `contract-2026-08-11-review-session-ui-events.md`、
|
|
12
|
+
`contract-2026-07-20-review-json-v1-annotation-model.md`
|
|
13
|
+
- スコープ: Theia shell の出力プレビュー、review session の `strokes.json` 読み出し、
|
|
14
|
+
compile-review-session と address-review の追跡導線
|
|
15
|
+
|
|
16
|
+
## 1. セッション中の表示
|
|
17
|
+
|
|
18
|
+
- pen / rect は従来どおり正規化座標(プレビューフレーム左上を `(0, 0)`、右下を `(1, 1)`)で
|
|
19
|
+
記録する。表示時に現在の content rect へ写像し直すため、ウィンドウのリサイズと出力比率の
|
|
20
|
+
変更で座標は変わらない。
|
|
21
|
+
- pointerup 後は従来のグロー・きらめき・600 ms フェードをそのまま再生し、その後段の静的
|
|
22
|
+
ビットマップへ同じ正規化図形を残す。新しい録音セッションの開始時に前セッションの表示を
|
|
23
|
+
クリアし、録音終了では消さない。
|
|
24
|
+
- 描線 canvas は非描画モードで `pointer-events: none` とする。ペンまたは四角モードのドラッグ中
|
|
25
|
+
だけ既存どおり入力面になる。
|
|
26
|
+
- 注釈パネルの「描線を表示」チェックは既定 ON。OFF は残留描線と明示的に再表示した描線を隠し、
|
|
27
|
+
データを削除しない。ON に戻すと保持した正規化座標から即時再描画する。
|
|
28
|
+
|
|
29
|
+
## 2. 既存セッションの読み出しと再表示
|
|
30
|
+
|
|
31
|
+
`readReviewSessionStrokes({projectRootUri, sessionId})` は
|
|
32
|
+
`review/sessions/<sessionId>/strokes.json` を読み、次を返す。
|
|
33
|
+
|
|
34
|
+
```jsonc
|
|
35
|
+
{
|
|
36
|
+
"sessionId": "s-0001",
|
|
37
|
+
"strokes": [/* pen / rect。frame と recTStart/recTEnd を保持 */],
|
|
38
|
+
"warnings": []
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- `strokes.json` 欠落は `strokes: []` として正常終了する。
|
|
43
|
+
- 配列ルート、または `version: 1` でない `{strokes:[]}` は旧形式として寛容に読む。
|
|
44
|
+
- JSON 破損、未知要素、値域外要素は描線単位で除外して warning に残す。セッション一覧と他の
|
|
45
|
+
描線を巻き込んで失敗させない。
|
|
46
|
+
- 注釈パネルの各録音済みセッションにある「描線」から再表示できる。表示メッセージには
|
|
47
|
+
`target.tab`(edit URI)と先頭ストロークの `target.recT` を添え、`frame.sourceT/cutIndex` で
|
|
48
|
+
プレビューを同じフレームへシークする。
|
|
49
|
+
|
|
50
|
+
## 3. compile 後の原本参照
|
|
51
|
+
|
|
52
|
+
review.json の data `version` は 0 のまま据え置く。compile-review-session はペアになった pen / rect
|
|
53
|
+
へ、既存フィールドを変えず次の任意フィールドを追加する。
|
|
54
|
+
|
|
55
|
+
```jsonc
|
|
56
|
+
"strokeRefs": [{
|
|
57
|
+
"sessionId": "s-0001",
|
|
58
|
+
"strokeId": "st-0001",
|
|
59
|
+
"sessionRef": "s-0001/st-0001"
|
|
60
|
+
}]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- `strokeRefs` は `null`、省略、または 1 件以上の配列。欠落は従来データとして正常である。
|
|
64
|
+
- pen は従来どおり最大 100 点の `strokes[].points` も埋め込み、`strokeRefs` から無加工の原本へ
|
|
65
|
+
戻れる。rect は従来どおり `targetKind:"region"` + `region.box` を埋め込み、同じ `strokeRefs`
|
|
66
|
+
から `strokes.json` 内の rect 原本へ戻れる。
|
|
67
|
+
- address-review の一覧は `strokeRefs` を
|
|
68
|
+
`review/sessions/<sessionId>/strokes.json#<strokeId>` として表示する。
|
|
69
|
+
|
|
70
|
+
## 4. 互換性
|
|
71
|
+
|
|
72
|
+
- 追加フィールドのみを使い、既存フィールドの削除・意味変更・data version bump は行わない。
|
|
73
|
+
- 読み手は未知フィールドを保持し、任意フィールドの欠落を旧データとして扱い、既知より大きい
|
|
74
|
+
data version を推測変換しない。
|
|
75
|
+
- `packages/preview-server` は本契約の対象外であり、WebUI の表示挙動は変更しない。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
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. Run them from a full AKARI Video app installation (normally ~/.akari/app) or a monorepo checkout.]",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|---|---|
|
|
10
10
|
| `report-helper.mjs` | `127.0.0.1` のみにバインドするローカル HTTP ヘルパー。`report.html` の配信・`decisions.json` の read/write・commit を仲介する |
|
|
11
11
|
| `report-template.html` | data 属性でカードを宣言する report.html の雛形。単選・真偽チェック・整数スライダ・配列型の複数選択に対応する。カード種別のハードコードなし。ヘルパー経由なら操作可能、`file://` 直開き等で fetch 失敗時は全操作ボタンが disabled になり案内文言を出す(安全劣化) |
|
|
12
|
+
| `render-research-plan-report.mjs` | `research-plan.json` から固定 5 面の自己完結 HTML を生成する。絵コンテ面は画像 / 文字プレースホルダーのカード面と、主軸 + カットアウェイの読み取り専用 SVG 構造面を切り替えられる |
|
|
12
13
|
| `examples/report.html` + `examples/report.html.decisions.json` | 動作サンプル一式(既存 4 カード + `direction`)。AI 推奨を既定値に入れた `decisions.json` 雛形付き |
|
|
13
14
|
| `test/direction-card.test.mjs` | ローカル Chrome を headless 起動して、演出カードの保存と既存 4 カードの非退行を実測するテスト |
|
|
14
15
|
|
|
@@ -54,6 +55,17 @@ curl -X POST http://127.0.0.1:8791/api/commit
|
|
|
54
55
|
npm test --workspace @akari-video/decision-cards
|
|
55
56
|
```
|
|
56
57
|
|
|
58
|
+
### research-plan のビジュアル絵コンテ
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
node packages/decision-cards/render-research-plan-report.mjs \
|
|
62
|
+
planning/research-plan.json \
|
|
63
|
+
planning/research-plan-report.html
|
|
64
|
+
node packages/decision-cards/report-helper.mjs planning/research-plan-report.html
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
画像は生成時に data URI へ埋め込むため、出力 HTML は外部依存を持たない。`image_path` が無い、または画像を読めないショットは `shot_type` + `description` のプレースホルダーへ安全に劣化する。旧形式に `sequence` / `cutaway_of` が無い場合も生成は成功し、構造面だけが「構造情報なし」になる。
|
|
68
|
+
|
|
57
69
|
## edit-plan スキルからの参照方法
|
|
58
70
|
|
|
59
71
|
公開リポの `.claude/skills/edit-plan/` は本パッケージを以下のように参照する想定:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@akari-video/decision-cards",
|
|
3
3
|
"version": "0.0.0",
|
|
4
4
|
"private": true,
|
|
5
|
-
"description": "決定カード v0 ランタイム。report-helper(ローカル HTTP サーバ)+ report-template(カード付きレポート HTML の雛形)+ 動作サンプル。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。 [akari-video npm vendor: report-helper.mjs is reference-only. These CLI entrypoints are not included in the akari-video npm package. Run them from a full AKARI Video app installation (normally ~/.akari/app) or a monorepo checkout.]",
|
|
5
|
+
"description": "決定カード v0 ランタイム。report-helper(ローカル HTTP サーバ)+ report-template(カード付きレポート HTML の雛形)+ 動作サンプル。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。 [akari-video npm vendor: report-helper.mjs is reference-only; render-research-plan-report.mjs is reference-only. These CLI entrypoints are not included in the akari-video npm package. Run them from a full AKARI Video app installation (normally ~/.akari/app) or a monorepo checkout.]",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"start": "node report-helper.mjs",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"akariVideoVendor": {
|
|
12
12
|
"execution": "reference-only",
|
|
13
13
|
"omittedBin": {
|
|
14
|
-
"decision-cards-helper": "report-helper.mjs"
|
|
14
|
+
"decision-cards-helper": "report-helper.mjs",
|
|
15
|
+
"research-plan-report": "render-research-plan-report.mjs"
|
|
15
16
|
},
|
|
16
17
|
"guidance": "These CLI entrypoints are not included in the akari-video npm package. Run them from a full AKARI Video app installation (normally ~/.akari/app) or a monorepo checkout."
|
|
17
18
|
}
|
|
@@ -19,6 +19,8 @@ import { musicGrid } from "../../audio-library-setup/shared/beat-grid.mjs";
|
|
|
19
19
|
import { resolveFfmpeg, resolveFfprobe } from "../../media-bin/src/index.mjs";
|
|
20
20
|
|
|
21
21
|
const {
|
|
22
|
+
areCutsAdjacent,
|
|
23
|
+
findUnsupportedDeclaredTrackTransitions,
|
|
22
24
|
projectLegacyEdit,
|
|
23
25
|
readInternalEdit,
|
|
24
26
|
resolveCaptionDisplay,
|
|
@@ -253,7 +255,7 @@ export async function lintProject(input, options = {}) {
|
|
|
253
255
|
validateStillImageCuts(edit, findings);
|
|
254
256
|
const cutTrackSegments = computeCutTrackSegments(edit.cuts);
|
|
255
257
|
for (const segment of findTrackOverlaps(cutTrackSegments)) {
|
|
256
|
-
if (isDeclaredTransitionOverlap(edit.cuts, cutTrackSegments, segment)) continue;
|
|
258
|
+
if (isDeclaredTransitionOverlap(edit.cuts, cutTrackSegments, segment, edit.fps)) continue;
|
|
257
259
|
addFinding(findings, {
|
|
258
260
|
severity: "error",
|
|
259
261
|
check: "cuts.track-overlap",
|
|
@@ -351,7 +353,11 @@ function collectAudioOnlySourceIds(edit) {
|
|
|
351
353
|
|
|
352
354
|
function collectInternalAudioTrackRefs(internalEdit) {
|
|
353
355
|
return new Set(internalEdit.tracks
|
|
354
|
-
|
|
356
|
+
// v2 top-level audio は読み込み層が implicit audio track へ射影するが、元データに
|
|
357
|
+
// tracks[] 宣言は無い。これを実段として数えると declaration-missing が必ず出る。
|
|
358
|
+
// validateTimelineTracks が照合する相手は projectLegacyEdit の declaredTracks なので、
|
|
359
|
+
// 実データ側も同じ declared origin に限定して投影ノイズを除く。
|
|
360
|
+
.filter(track => track.origin === "declared" && track.lane === "audio" && track.items.length > 0)
|
|
355
361
|
.map(track => Number.isInteger(track.legacy.ref) ? track.legacy.ref : 0));
|
|
356
362
|
}
|
|
357
363
|
|
|
@@ -878,15 +884,18 @@ function findTrackOverlaps(segments) {
|
|
|
878
884
|
// render correctly would still fail lint. Still rejects zero/negative overlap (a genuine gap --
|
|
879
885
|
// no transition is physically possible) and overlap greater than declared (an unrelated shape
|
|
880
886
|
// render-cut does not auto-adjust for -- see effectiveTransitionDurations' own comment).
|
|
881
|
-
function isDeclaredTransitionOverlap(cuts, segments, current) {
|
|
887
|
+
function isDeclaredTransitionOverlap(cuts, segments, current, fps) {
|
|
882
888
|
const previous = segments
|
|
883
889
|
.filter(segment => segment.track === current.track && segment.index < current.index)
|
|
884
890
|
.sort((left, right) => right.index - left.index)[0];
|
|
885
891
|
if (!previous) return false;
|
|
886
892
|
const duration = cuts?.[previous.index]?.transition_out?.duration;
|
|
887
893
|
if (!isPositiveNumber(duration)) return false;
|
|
888
|
-
|
|
889
|
-
|
|
894
|
+
return areCutsAdjacent(
|
|
895
|
+
{ tlEnd: previous.end, transitionOut: { duration } },
|
|
896
|
+
{ tlStart: current.start },
|
|
897
|
+
fps,
|
|
898
|
+
);
|
|
890
899
|
}
|
|
891
900
|
|
|
892
901
|
function validateCutTrackFields(cuts, findings) {
|
|
@@ -1255,15 +1264,16 @@ function validateTimelineTracks(edit, findings, projectedAudioTracks = null) {
|
|
|
1255
1264
|
const ref = item.kind === "audio" && !hasRef ? 0 : item.ref;
|
|
1256
1265
|
if (ref === undefined) continue;
|
|
1257
1266
|
declarations.add(`${item.kind}:${ref}`);
|
|
1258
|
-
if (!actualTracks.get(item.kind)?.has(ref)) {
|
|
1259
|
-
addFinding(findings, {
|
|
1260
|
-
severity: "warning",
|
|
1261
|
-
check: "timeline.tracks.ref-missing",
|
|
1262
|
-
message: `${item.kind} timeline track ref ${ref} is not used by edit data`,
|
|
1263
|
-
path: `${path}.ref`,
|
|
1264
|
-
});
|
|
1265
|
-
}
|
|
1266
1267
|
}
|
|
1268
|
+
// ここには以前 `timeline.tracks.ref-missing`(宣言された段の ref が実データのどこにも
|
|
1269
|
+
// 現れなければ警告)があったが、2026-08-20 に撤去した。v2 では timeline.tracks[] の各段が
|
|
1270
|
+
// internal-model.ts の projectLegacyEdit を通じてそのまま legacy 射影され、ref は宣言順に
|
|
1271
|
+
// 毎回生成し直される連番なので、「宣言はあるが実データに現れない ref」は「段の中身が 0 個」
|
|
1272
|
+
// としか等価にならない。空の段は自動 prune せず残すのが正本(10番裁定 E)なので、この
|
|
1273
|
+
// チェックは空の段を持つ v2 プロジェクトのたびに必ず誤検知していた。v0/v1 は本体から既に
|
|
1274
|
+
// 除かれており(9番)、「(kind, ref) の参照」という v0/v1 由来の概念自体が v2 には無いため
|
|
1275
|
+
// 部分修正ではなく撤去する。撤去の証跡は edit-lint.test.mjs の
|
|
1276
|
+
// "空の段を持つ v2 プロジェクトは findings 0" で固定してある。
|
|
1267
1277
|
|
|
1268
1278
|
for (const [kind, tracks] of actualTracks) {
|
|
1269
1279
|
for (const ref of tracks) {
|
|
@@ -1295,56 +1305,27 @@ function validateTimelineTracks(edit, findings, projectedAudioTracks = null) {
|
|
|
1295
1305
|
// too wide a blast radius to take on speculatively, especially with no evidence anyone needs the
|
|
1296
1306
|
// combination. Reject it instead: it fails loudly and specifically, rather than rendering a
|
|
1297
1307
|
// broken video with a phantom black flash that's very hard to trace back to its cause.
|
|
1298
|
-
function validateTrackTransitionOutCompatibility(edit, findings) {
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
for (const { cut, index } of trackCuts.slice(0, -1)) {
|
|
1317
|
-
if (!cut.transition_out) continue;
|
|
1318
|
-
addFinding(findings, {
|
|
1319
|
-
severity: "error",
|
|
1320
|
-
check: "cuts.track-transition-unsupported",
|
|
1321
|
-
message:
|
|
1322
|
-
`cuts[].transition_out is declared on track ${ref}, which timeline.tracks composites through the `
|
|
1323
|
-
+ `gap-aware track engine. That engine treats adjacent same-track cuts as separate, non-overlapping `
|
|
1324
|
-
+ `windows, so it cannot represent an xfade's intentional overlap -- the composited window and the `
|
|
1325
|
-
+ `actually-shrunk clip diverge, and content disappears early (verified with a real render: the base `
|
|
1326
|
-
+ `track's background visibly leaked through where the dissolved clip should still have been `
|
|
1327
|
-
+ `playing). Remove transition_out from this track's cuts, or drop the custom timeline.tracks order `
|
|
1328
|
-
+ `for this track so it renders through the plain sequential path instead.`,
|
|
1329
|
-
path: `edit.json#cuts[${index}]`,
|
|
1330
|
-
});
|
|
1331
|
-
}
|
|
1308
|
+
export function validateTrackTransitionOutCompatibility(edit, findings) {
|
|
1309
|
+
for (const { cutIndex, trackRef } of findUnsupportedDeclaredTrackTransitions(
|
|
1310
|
+
edit?.cuts,
|
|
1311
|
+
edit?.timeline?.tracks,
|
|
1312
|
+
)) {
|
|
1313
|
+
addFinding(findings, {
|
|
1314
|
+
severity: "error",
|
|
1315
|
+
check: "cuts.track-transition-unsupported",
|
|
1316
|
+
message:
|
|
1317
|
+
`cuts[].transition_out is declared on track ${trackRef}, which timeline.tracks composites through the `
|
|
1318
|
+
+ `gap-aware track engine. That engine treats adjacent same-track cuts as separate, non-overlapping `
|
|
1319
|
+
+ `windows, so it cannot represent an xfade's intentional overlap -- the composited window and the `
|
|
1320
|
+
+ `actually-shrunk clip diverge, and content disappears early (verified with a real render: the base `
|
|
1321
|
+
+ `track's background visibly leaked through where the dissolved clip should still have been `
|
|
1322
|
+
+ `playing). Remove transition_out from this track's cuts, or drop the custom timeline.tracks order `
|
|
1323
|
+
+ `for this track so it renders through the plain sequential path instead.`,
|
|
1324
|
+
path: `edit.json#cuts[${cutIndex}]`,
|
|
1325
|
+
});
|
|
1332
1326
|
}
|
|
1333
1327
|
}
|
|
1334
1328
|
|
|
1335
|
-
function usesDefaultCompatibilityTrackOrder(tracks) {
|
|
1336
|
-
const rank = new Map([["cuts", 0], ["layers", 1], ["overlays", 2], ["captions", 3], ["audio", 4]]);
|
|
1337
|
-
const keys = tracks.map((track, index) => ({
|
|
1338
|
-
kind: track?.kind,
|
|
1339
|
-
ref: Number.isInteger(track?.ref) ? track.ref : -1,
|
|
1340
|
-
index,
|
|
1341
|
-
}));
|
|
1342
|
-
if (keys.some(key => !rank.has(key.kind))) return false;
|
|
1343
|
-
const expected = [...keys].sort((left, right) =>
|
|
1344
|
-
rank.get(left.kind) - rank.get(right.kind) || left.ref - right.ref || left.index - right.index);
|
|
1345
|
-
return keys.every((key, index) => key.index === expected[index].index);
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
1329
|
function collectActualTrackNumbers(items) {
|
|
1349
1330
|
const tracks = new Set();
|
|
1350
1331
|
if (!Array.isArray(items)) return tracks;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cuts 配列順で隣り合う同一トラックの 2 クリップが、トランジション UI を持てる境界か判定する。
|
|
3
|
+
* タイムライン UI と edit-lint は必ずこの関数を使い、フレーム量子化の条件式を複製しない。
|
|
4
|
+
*/
|
|
5
|
+
export interface CutAdjacencyTransitionLike {
|
|
6
|
+
duration?: unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface CutAdjacencyEarlierLike {
|
|
9
|
+
tlEnd: number;
|
|
10
|
+
transitionOut?: CutAdjacencyTransitionLike | null;
|
|
11
|
+
}
|
|
12
|
+
export interface CutAdjacencyLaterLike {
|
|
13
|
+
tlStart: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* ギャップ 0、または earlier の宣言済み transitionOut.duration で説明できる重なりだけを
|
|
17
|
+
* 隣接とみなす。秒の誤差ではなく、出力 fps で量子化したフレーム数を比較する。
|
|
18
|
+
*/
|
|
19
|
+
export declare function areCutsAdjacent(earlier: CutAdjacencyEarlierLike, later: CutAdjacencyLaterLike, fps?: number): boolean;
|