akari-video 0.1.10 → 0.1.12

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 (86) hide show
  1. package/bin/akari.mjs +2 -0
  2. package/package.json +2 -2
  3. package/src/messages.mjs +1 -0
  4. package/src/migrate-command.mjs +127 -0
  5. package/src/status-core/status.mjs +35 -26
  6. package/vendor/.akari-capability-sources.json +1 -0
  7. package/vendor/docs/contract-2026-07-17-data-contract-versioning.md +27 -0
  8. package/vendor/docs/contract-2026-07-22-render-basics.md +29 -1
  9. package/vendor/docs/contract-2026-07-23-analysis-person-matte.md +7 -0
  10. package/vendor/docs/contract-2026-07-25-r6-audio-tracks-and-trim.md +41 -0
  11. package/vendor/docs/contract-2026-08-02-preview-parity.md +18 -1
  12. package/vendor/docs/contract-2026-08-03-caption-display-encoding-qc-v1.md +34 -0
  13. package/vendor/docs/contract-2026-08-12-still-image-cut-source-v0.md +13 -0
  14. package/vendor/docs/contract-2026-08-13-avatar-drive-v0.md +246 -0
  15. package/vendor/docs/contract-2026-08-18-v1-render-parity.md +115 -0
  16. package/vendor/packages/akari-launcher/package.json +2 -2
  17. package/vendor/packages/asset-resolver/src/paid-zip.mjs +6 -1
  18. package/vendor/packages/asset-resolver/src/resolve.mjs +28 -6
  19. package/vendor/packages/asset-resolver/test/resolve-paid-zip.test.mjs +93 -3
  20. package/vendor/packages/audio-library-setup/README.md +2 -2
  21. package/vendor/packages/audio-library-setup/bin/beat-grid.mjs +20 -12
  22. package/vendor/packages/audio-library-setup/shared/beat-grid.mjs +60 -11
  23. package/vendor/packages/audio-library-setup/test/beat-grid.test.mjs +58 -24
  24. package/vendor/packages/edit-lint/src/cut-timeline.mjs +17 -0
  25. package/vendor/packages/edit-lint/src/edit-lint.mjs +386 -277
  26. package/vendor/packages/edit-store/lib/caption-display.js +4 -2
  27. package/vendor/packages/edit-store/lib/edit-store.d.ts +11 -21
  28. package/vendor/packages/edit-store/lib/edit-store.js +25 -531
  29. package/vendor/packages/edit-store/lib/edit-v2.d.ts +147 -0
  30. package/vendor/packages/edit-store/lib/edit-v2.js +296 -0
  31. package/vendor/packages/edit-store/lib/index.d.ts +6 -0
  32. package/vendor/packages/edit-store/lib/index.js +12 -0
  33. package/vendor/packages/edit-store/lib/internal-model.d.ts +183 -0
  34. package/vendor/packages/edit-store/lib/internal-model.js +492 -0
  35. package/vendor/packages/edit-store/lib/migrate/error.d.ts +4 -0
  36. package/vendor/packages/edit-store/lib/migrate/error.js +13 -0
  37. package/vendor/packages/edit-store/lib/migrate/index.d.ts +53 -0
  38. package/vendor/packages/edit-store/lib/migrate/index.js +404 -0
  39. package/vendor/packages/edit-store/lib/migrate/legacy-parse.d.ts +31 -0
  40. package/vendor/packages/edit-store/lib/migrate/legacy-parse.js +552 -0
  41. package/vendor/packages/edit-store/lib/retime.d.ts +9 -0
  42. package/vendor/packages/edit-store/lib/retime.js +79 -0
  43. package/vendor/packages/edit-store/lib/timeline-map.d.ts +1 -8
  44. package/vendor/packages/edit-store/lib/timeline-map.js +1 -48
  45. package/vendor/packages/edit-store/lib/track-order.d.ts +38 -0
  46. package/vendor/packages/edit-store/lib/track-order.js +63 -0
  47. package/vendor/packages/edit-store/lib/webview-kernel.js +0 -37
  48. package/vendor/packages/edit-store/lib/write-gate.d.ts +41 -15
  49. package/vendor/packages/edit-store/lib/write-gate.js +91 -70
  50. package/vendor/packages/media-bin/package.json +2 -1
  51. package/vendor/packages/media-bin/scripts/build-whisper.mjs +224 -0
  52. package/vendor/packages/media-bin/scripts/fetch-binaries.mjs +22 -1
  53. package/vendor/packages/media-bin/src/binary-manifest.mjs +60 -3
  54. package/vendor/packages/media-bin/src/index.mjs +71 -17
  55. package/vendor/packages/media-bin/test/whisper.test.mjs +142 -0
  56. package/vendor/packages/project-scaffold/src/index.mjs +10 -1
  57. package/vendor/packages/project-scaffold/test/create-project.test.mjs +10 -2
  58. package/vendor/packages/schemas/bin/validate-edit.mjs +14 -3
  59. package/vendor/packages/schemas/edit.schema.json +266 -7
  60. package/vendor/packages/schemas/examples/edit-sfx-fade-invalid/edit.json +10 -0
  61. package/vendor/packages/schemas/examples/edit-sfx-in-out-valid/edit.json +1 -1
  62. package/vendor/packages/schemas/examples/edit-v2-fractional-fps-invalid/edit.json +6 -0
  63. package/vendor/packages/schemas/examples/edit-v2-html-in-out-invalid/edit.json +19 -0
  64. package/vendor/packages/schemas/examples/edit-v2-item-kind-field-invalid/edit.json +20 -0
  65. package/vendor/packages/schemas/examples/edit-v2-items-content-invalid/edit.json +13 -0
  66. package/vendor/packages/schemas/examples/edit-v2-minimal-valid/edit.json +6 -0
  67. package/vendor/packages/schemas/examples/edit-v2-telop-in-out-invalid/edit.json +19 -0
  68. package/vendor/packages/schemas/examples/edit-v2-valid/edit.json +86 -0
  69. package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +78 -0
  70. package/vendor/packages/schemas/test/validate-edit.test.mjs +19 -2
  71. package/vendor/skills/analyze-footage/media-and-transcript.md +15 -10
  72. package/vendor/skills/edit-plan/SKILL.md +13 -11
  73. package/vendor/skills/edit-plan/approvals-and-generation.md +2 -2
  74. package/vendor/skills/edit-plan/beat-sync.md +52 -104
  75. package/vendor/skills/edit-plan/beats.md +11 -9
  76. package/vendor/skills/edit-plan/emphasis-detection.md +11 -44
  77. package/vendor/skills/edit-plan/execution.md +91 -78
  78. package/vendor/skills/edit-plan/report-guide.md +4 -4
  79. package/vendor/skills/edit-plan/workflow.md +1 -1
  80. package/vendor/skills/export-nle/SKILL.md +26 -6
  81. package/vendor/skills/overlay-authoring/3d.md +47 -1
  82. package/vendor/skills/overlay-authoring/SKILL.md +13 -1
  83. package/vendor/skills/overlay-authoring/motion.md +4 -17
  84. package/vendor/skills/setup-library/SKILL.md +1 -1
  85. package/vendor/skills/setup-library/tools-check.md +74 -17
  86. package/vendor/skills/verify/SKILL.md +26 -16
package/bin/akari.mjs CHANGED
@@ -10,6 +10,7 @@ import { runAcceptCommand } from '../src/accept-command.mjs';
10
10
  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
+ import { runMigrateCommand } from '../src/migrate-command.mjs';
13
14
  import { maybeApplyPendingUpdateOnLaunch, readOwnVersion } from '../src/update-check.mjs';
14
15
  import { describeCliHelp } from '../src/messages.mjs';
15
16
 
@@ -67,6 +68,7 @@ const invoke = (argv[0] === '--version' || argv[0] === '-v') ? printVersion()
67
68
  : argv[0] === 'capability' ? runCapabilityCommand(argv.slice(1))
68
69
  : argv[0] === 'store' ? runStoreCommand(argv.slice(1))
69
70
  : argv[0] === 'assets' ? runAssetsCommand(argv.slice(1))
71
+ : argv[0] === 'migrate' ? runMigrateCommand(argv.slice(1))
70
72
  : run(argv);
71
73
 
72
74
  const result = await invoke.catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
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": {
@@ -37,6 +37,6 @@
37
37
  "scripts": {
38
38
  "prepack": "node scripts/prepack.mjs",
39
39
  "postpack": "node scripts/prepack.mjs clean",
40
- "test": "node --test test/*.mjs"
40
+ "test": "node --test --test-concurrency=1 test/*.mjs"
41
41
  }
42
42
  }
package/src/messages.mjs CHANGED
@@ -224,6 +224,7 @@ export function describeCliHelp() {
224
224
  ' sounds 公式音源ライブラリを一括ダウンロード(無料)',
225
225
  ' update 更新を確認する',
226
226
  ' status 接続状態を確認する',
227
+ ' migrate [dir] 古い edit.json を退避バックアップ付きで v2 へ変換',
227
228
  '',
228
229
  '開発者向け:',
229
230
  ' --opencode Claude Code の代わりに opencode を起動する',
@@ -0,0 +1,127 @@
1
+ import { createRequire } from 'node:module';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { createInterface } from 'node:readline/promises';
5
+
6
+ import { resolveLauncherAssets } from './repo-assets.mjs';
7
+
8
+ const require = createRequire(import.meta.url);
9
+
10
+ export async function runMigrateCommand(args, options = {}) {
11
+ const log = options.log ?? ((line) => console.log(line));
12
+ const error = options.error ?? ((line) => console.error(line));
13
+ const cwd = options.cwd ?? process.cwd();
14
+ const parsed = parseArguments(args, cwd);
15
+ if (!parsed.ok) {
16
+ error(parsed.message);
17
+ return { exitCode: 2 };
18
+ }
19
+ if (parsed.help) {
20
+ for (const line of migrateHelp()) log(line);
21
+ return { exitCode: 0 };
22
+ }
23
+ const projectRoot = parsed.projectRoot;
24
+ const editPath = path.join(projectRoot, 'edit.json');
25
+ let text;
26
+ try {
27
+ text = await (options.readFile ?? readFile)(editPath, 'utf8');
28
+ } catch (cause) {
29
+ error(`edit.json を読めません: ${editPath} (${messageOf(cause)})`);
30
+ return { exitCode: 2 };
31
+ }
32
+ const migrate = options.migrate ?? loadMigrateModule(options.assets ?? resolveLauncherAssets());
33
+ const proposal = migrate.planMigration(projectRoot, editPath, text, { now: options.now });
34
+ if (proposal.ok === false) {
35
+ if (parsed.json) {
36
+ log(JSON.stringify({ ok: false, error: 'このプロジェクトは変換できません', blockers: proposal.blockers }));
37
+ } else {
38
+ error('このプロジェクトは変換できません。');
39
+ for (const blocker of proposal.blockers) error(`- ${blocker}`);
40
+ }
41
+ return { exitCode: 2 };
42
+ }
43
+ if (parsed.json) {
44
+ log(JSON.stringify({
45
+ ok: true, dryRun: parsed.dryRun, version: proposal.version,
46
+ filePath: proposal.filePath, backupPath: proposal.backupPath, changes: proposal.changes
47
+ }));
48
+ } else {
49
+ log(`変換対象: ${proposal.filePath} (version ${proposal.version} -> 2)`);
50
+ for (const change of proposal.changes) log(`- ${change.path}: ${change.note}`);
51
+ log(`変換前の退避先: ${proposal.backupPath}`);
52
+ }
53
+ if (parsed.dryRun) {
54
+ if (!parsed.json) log('--dry-run のため、ファイルは変更しません。');
55
+ return { exitCode: 0, proposal };
56
+ }
57
+ if (!parsed.yes) {
58
+ const isTTY = options.isTTY ?? (process.stdin.isTTY && process.stdout.isTTY);
59
+ if (!isTTY) {
60
+ error('非 TTY では明示承認が必要です。内容を確認し、--yes を付けて再実行してください。');
61
+ return { exitCode: 2, proposal };
62
+ }
63
+ const accepted = options.confirm
64
+ ? await options.confirm()
65
+ : await promptForConfirmation();
66
+ if (!accepted) {
67
+ if (!parsed.json) log('変換しませんでした。edit.json は変更されていません。');
68
+ return { exitCode: 0, proposal };
69
+ }
70
+ }
71
+ await migrate.applyMigration(proposal);
72
+ if (!parsed.json) log(`version 2 へ変換しました。元ファイル: ${proposal.backupPath}`);
73
+ return { exitCode: 0, proposal };
74
+ }
75
+
76
+ function loadMigrateModule(assets) {
77
+ const modulePath = path.join(assets.repoRoot, 'packages', 'edit-store', 'lib', 'migrate', 'index.js');
78
+ try {
79
+ return require(modulePath);
80
+ } catch (cause) {
81
+ throw new Error(`変換器を読み込めません: ${modulePath} (${messageOf(cause)})`);
82
+ }
83
+ }
84
+
85
+ function parseArguments(args, cwd) {
86
+ const flags = new Set(args.filter(value => value.startsWith('-')));
87
+ const unknown = [...flags].filter(value => !['--yes', '-y', '--dry-run', '--json', '--help', '-h'].includes(value));
88
+ if (unknown.length > 0) return { ok: false, message: `未知のオプションです: ${unknown.join(', ')}` };
89
+ const positional = args.filter(value => !value.startsWith('-'));
90
+ if (positional.length > 1) return { ok: false, message: '引数のプロジェクトディレクトリは 1 つだけ指定できます。' };
91
+ return {
92
+ ok: true,
93
+ help: flags.has('--help') || flags.has('-h'),
94
+ yes: flags.has('--yes') || flags.has('-y'),
95
+ dryRun: flags.has('--dry-run'),
96
+ json: flags.has('--json'),
97
+ projectRoot: path.resolve(cwd, positional[0] ?? '.')
98
+ };
99
+ }
100
+
101
+ async function promptForConfirmation() {
102
+ const readline = createInterface({ input: process.stdin, output: process.stdout });
103
+ try {
104
+ const answer = await readline.question('上記の内容で version 2 へ変換しますか? [y/N] ');
105
+ return /^(?:y|yes)$/iu.test(answer.trim());
106
+ } finally {
107
+ readline.close();
108
+ }
109
+ }
110
+
111
+ export function migrateHelp() {
112
+ return [
113
+ '使い方: akari migrate [dir] [--yes] [--dry-run] [--json]',
114
+ '',
115
+ 'v0/v1 の edit.json を v2 へ片道変換します。',
116
+ '既定は変更内容を表示して y/n で確認し、変換前の全文を .akari/backup/ へ退避します。',
117
+ '',
118
+ ' --yes, -y 表示後の確認を省略',
119
+ ' --dry-run 提案の表示だけで書き込まない',
120
+ ' --json 機械可読な JSON を出力',
121
+ ' --help, -h このヘルプを表示'
122
+ ];
123
+ }
124
+
125
+ function messageOf(error) {
126
+ return error instanceof Error ? error.message : String(error);
127
+ }
@@ -7,6 +7,7 @@ import { resolveProjectDisplayName } from "./display-name.mjs";
7
7
  import { inspectFullIntegrity } from "./integrity.mjs";
8
8
  import { validateAndCountReview } from "./review.mjs";
9
9
 
10
+
10
11
  export {
11
12
  detectStatusScope,
12
13
  resolveWorkspaceStatus,
@@ -218,30 +219,38 @@ function validatePlan(value, problems) {
218
219
 
219
220
  function validateEdit(value, problems) {
220
221
  if (!value) return;
221
- if (value.version !== 0 && value.version !== 1) {
222
+ if (value.version !== 2) {
222
223
  problems.push(`edit.json has unsupported version ${String(value.version)}`);
223
224
  return;
224
225
  }
225
- if (!isRecord(value.output) || !Array.isArray(value.overlays)) problems.push("edit.json has an invalid output/overlays shape");
226
- if (value.version === 0) {
227
- if (!isNonEmptyString(value.source?.path) || Object.hasOwn(value, "sources")) problems.push("edit.json v0 source shape is invalid");
228
- } else {
229
- if (!Array.isArray(value.sources) || Object.hasOwn(value, "source") || !Array.isArray(value.cuts)) {
230
- problems.push("edit.json v1 sources/cuts shape is invalid");
226
+ if (!isRecord(value.output) || !Array.isArray(value.sources) || !Array.isArray(value.tracks)) {
227
+ problems.push("edit.json v2 has an invalid output/sources/tracks shape");
228
+ return;
229
+ }
230
+ const sourceIds = new Set();
231
+ for (const source of value.sources) {
232
+ if (!isNonEmptyString(source?.id) || !isNonEmptyString(source?.path) || sourceIds.has(source.id)) {
233
+ problems.push("edit.json v2 sources are invalid or duplicated");
231
234
  return;
232
235
  }
233
- const ids = new Set();
234
- for (const source of value.sources) {
235
- if (!isNonEmptyString(source?.id) || !isNonEmptyString(source?.path) || ids.has(source.id)) {
236
- problems.push("edit.json v1 sources are invalid or duplicated");
237
- break;
238
- }
239
- ids.add(source.id);
236
+ sourceIds.add(source.id);
237
+ }
238
+ const trackIds = new Set();
239
+ for (const track of value.tracks) {
240
+ const hasItems = Array.isArray(track?.items);
241
+ const hasContent = isRecord(track?.content);
242
+ if (!isNonEmptyString(track?.id) || trackIds.has(track.id)
243
+ || (track?.lane !== "visual" && track?.lane !== "audio") || hasItems === hasContent) {
244
+ problems.push("edit.json v2 tracks are invalid or duplicated");
245
+ return;
240
246
  }
241
- for (const cut of value.cuts) {
242
- if (!isRecord(cut) || !ids.has(cut.src)) {
243
- problems.push(`edit.json cut references unknown source ${String(cut?.src)}`);
244
- break;
247
+ trackIds.add(track.id);
248
+ if (!hasItems) continue;
249
+ for (const item of track.items) {
250
+ if (!isRecord(item) || !isRecord(item.source)
251
+ || (item.source.kind === "media" && !sourceIds.has(item.source.src))) {
252
+ problems.push(`edit.json v2 item references unknown source ${String(item?.source?.src)}`);
253
+ return;
245
254
  }
246
255
  }
247
256
  }
@@ -270,14 +279,14 @@ function resolveMaterialState({ projectRoot, edit, interpretation, problems, war
270
279
  let fixed = false;
271
280
  if (edit) {
272
281
  fixed = true;
273
- if (edit.version === 0 && isNonEmptyString(edit.source?.path)) {
274
- sources = [resolveProjectPath(projectRoot, edit.source.path, "edit source", problems)];
275
- } else if (edit.version === 1 && Array.isArray(edit.sources) && Array.isArray(edit.cuts)) {
276
- const usedIds = new Set(edit.cuts.map((cut) => cut?.src));
277
- sources = edit.sources
278
- .filter((source) => usedIds.has(source.id))
279
- .map((source) => resolveProjectPath(projectRoot, source.path, `edit source ${source.id}`, problems));
280
- }
282
+ const mainVisualTrack = Array.isArray(edit.tracks)
283
+ ? edit.tracks.find(track => track?.lane === "visual" && Array.isArray(track.items)) : null;
284
+ const usedIds = new Set((mainVisualTrack?.items ?? [])
285
+ .filter(item => item?.source?.kind === "media")
286
+ .map(item => item.source.src));
287
+ sources = (Array.isArray(edit.sources) ? edit.sources : [])
288
+ .filter(source => usedIds.has(source?.id) && isNonEmptyString(source?.path))
289
+ .map(source => resolveProjectPath(projectRoot, source.path, `edit source ${source.id}`, problems));
281
290
  } else if (interpretation && Array.isArray(interpretation.inputs?.analyses)) {
282
291
  fixed = true;
283
292
  for (const [index, entry] of interpretation.inputs.analyses.entries()) {
@@ -39,6 +39,7 @@
39
39
  "docs/contract-2026-08-12-still-image-cut-source-v0.md",
40
40
  "docs/contract-2026-08-13-avatar-drive-v0.md",
41
41
  "docs/contract-2026-08-14-avatar-vrm-v0.md",
42
+ "docs/contract-2026-08-18-v1-render-parity.md",
42
43
  "packages/akari-launcher/package.json",
43
44
  "packages/akari-launcher/README.md",
44
45
  "packages/akari-tools/package.json",
@@ -64,3 +64,30 @@
64
64
  2. 「追加のみ進化・tolerant reader」を契約文書に明記する
65
65
  3. 破壊的変更時の変換手順を書く節を契約文書に確保する(bump するまで空でよい)
66
66
  4. 読み手の forward-compat 挙動(原則 3)を実装する
67
+
68
+ ## 5. 契約ファイル名の `-vN` と edit.json スキーマ版の語彙衝突(2026-08-18 追記)
69
+
70
+ 契約ファイル名の `-vN` サフィックス(例: `contract-2026-08-12-still-image-cut-source-v0.md`)は
71
+ **その契約文書自身のリビジョン**であり、本書が定めるデータ契約の `version` フィールド
72
+ (edit.json 等のスキーマ版)とは無関係である。両者は独立に増減する — 契約文書が改訂されても
73
+ edit.json の `version` は変わらないし、edit.json が `version: 1` に上がっても関連契約が
74
+ `-v1.md` へリネームされるわけではない。
75
+
76
+ 「still-image-cut-source-v0(契約の第 1 版)を読んで edit.json は v1(スキーマ版)で書く」の
77
+ ような読み替えは誤読を招く。edit.json のスキーマ版に言及する契約文書は、本文冒頭で
78
+ **「edit v0」「edit v1」の表記**を使い分け、ファイル名の `-vN` と区別できるようにする(例:
79
+ 「本契約は edit v1 の `sources[]` を対象とする」)。読み手(エージェント・人間)も、契約
80
+ ファイル名の `-vN` を見て自動的にスキーマ版だと解釈しない。
81
+
82
+ ## 6. edit.json v0/v1 凍結変換器の終了方針(2026-08-18 オーナー裁定)
83
+
84
+ - 変換器は機能追加禁止・バグ修正のみとする。未知の v0/v1 ケースに対応を
85
+ 追加せず、「このプロジェクトは変換できません」と理由付きで止まる。
86
+ - AKARI Video 本体から変換器を外す期限は、製品版 `1.0.0` の公開日または
87
+ `2026-12-31` のいずれか早い方とする。
88
+ - 期限到達時は変換器を破棄せず、単体 npm パッケージ `akari-migrate` へ切り出して
89
+ 1 回だけ publish し、以後更新しない。publish 自体は本移行タスクでは行わず、
90
+ 本体から外す作業の一部とする。
91
+ - 本体から外した後のエラー文言は次で固定する。
92
+
93
+ > このプロジェクトは古い形式です。`npx akari-migrate@<版> <dir>` で変換してから開いてください。
@@ -17,7 +17,7 @@
17
17
  |---|---|---|---|---|
18
18
  | 1 | 定速変更(クリップ単位の倍速/スロー) | `cuts[].speed`(number・既定 1.0・v0 は定速のみ、ランプは将来) | `setpts` + `atempo`(>2x/<0.5x の段組み) | 出力尺が理論値と一致(ffprobe)・音程/同期の実聴確認 1 点 |
19
19
  | 2 | クロマキー背景置換 | `source.chroma_key`: {color, similarity, blend, background(色 or 画像/動画パス)} | `chromakey`/`colorkey` + 背景入力の `overlay` | 緑背景フィクスチャで背景が置換された出力のピクセルサンプル検証 |
20
- | 3 | 基本トランジション | `cuts[].transition_out`: {type: dissolve/fade-black/fade-white, duration} | `xfade`(transition 指定があるカット境界のみ xfade 経路) | 境界フレームの中間ブレンド実在をフレーム抽出で確認・指定なし境界はハードカット維持 |
20
+ | 3 | 基本トランジション | `cuts[].transition_out`: {type: dissolve/fade-black/fade-white/**reveal-down/reveal-up**, duration} | `xfade`(transition 指定があるカット境界のみ xfade 経路)。reveal 系は ffmpeg の `revealdown` / `revealup` | 境界フレームの中間ブレンド実在をフレーム抽出で確認・指定なし境界はハードカット維持。reveal 系は色が混ざらないため、遷移中間フレームの**上半分と下半分を別々に測って**前後カットが同居することを確認する |
21
21
  | 4 | 色調フィルター(LUT) | `output.look`: {lut(プリセット参照 or パス), intensity} | `lut3d`(intensity は `blend` 併用) | LUT 有無 2 出力のフレームピクセル差分・プリセット表 `presets/luts/`(初期 2〜3 本。2026-07-29 に `catalog/luts/` から移設) |
22
22
  | 5 | 音声マスター処理 | `audio.master`: {denoise(off/std/strong), loudnorm(target LUFS・既定 -14)} | `afftdn` / `loudnorm`(2 パスでなく 1 パス許容 v0) | 出力のラウドネス実測(ffmpeg ebur128)が目標 ±1LU |
23
23
  | 6 | 画角操作(静的クロップ / ズームキーフレーム / 段階縮小) | `cuts[].framing`: `{crop?: {x,y,w,h}(0..1 の出力相対・静的), keyframes?: [{t,scale,cx?,cy?}](t=カット内秒・線形補間。2 点でズーム、3 点以上で段階縮小・cx/cy 省略時 0.5)}` | 出力キャンバスへフィット済みの frame を `crop` で窓抜きし `scale` で再拡大(punch-in)。静的 `crop` は `w/h/x/y` とも定数。ズームは `crop` 自身の `w/h` が実機検証で init 時一度しか評価されない制約があるため、`scale` 側を `eval=frame` で `scale(t)` 倍に広げ、`crop` は固定 `w=width:h=height` のまま `x/y` だけを `t` の関数で追わせる方式(詳細 §4-1) | 静的 crop は出力フレームの画素でクロップ位置が宣言どおりであることを実測・ズームは開始/中間/終端フレームで可視要素の実測サイズから逆算したスケールが線形補間の理論値と一致(±5%)・3 点キーフレームは 2 段階の縮小がフレーム抽出で確認できる |
@@ -31,6 +31,32 @@
31
31
  edit-lint / fixtures / test を同時追随
32
32
  2. プレビュー(preview-engine)は v0 では**近似不要・無視でよい**(出力最優先。
33
33
  「プレビューは近似・書き出しが正」の哲学を全項目に適用。プレビュー追随は別契約)
34
+ 3. **`output.look`(#4 の LUT)の適用範囲は `cuts[]` の本編映像だけ**である。
35
+ `layers[]`(PinP / 人物マット / B-roll)と `overlays[]` には**掛からない**。
36
+ 同じ絵の一部として重ねる素材の色を本編に合わせたいときは、`layers[].filter`
37
+ (`{type:"lut", id, intensity}`。正本 = `contract-2026-08-12-region-filter-layer-v0.md` §4)
38
+ へ**同じ `id` / `intensity` を明示的に宣言する**。
39
+ 実害例(2026-08-14・リール制作): 本編にだけ `cinematic` が乗り、重ねた人物切り抜きが
40
+ 素の色のまま合成されて、窓の継ぎ目で肌色が食い違った。「プロジェクト全体の色」だと
41
+ 誤解しやすいため、ここに明記する。
42
+
43
+ ### 2-4. reveal 系トランジション(`reveal-down` / `reveal-up`。2026-08-14 追加)
44
+
45
+ **前カットが丸ごとその方向へ動いて画面外へ抜け、空いた側から次カットが現れる**
46
+ (前カットは動きながら画面端でクロップされる)。ディゾルブのように混ざらないので、
47
+ **同じ構図が続くトークシーンでも「場面が入れ替わった」ことが読める**のが採用理由
48
+ (オーナー指定 2026-08-14「テンプレの基本トランジションとして必要」)。
49
+
50
+ - `reveal-down` = 前カットが下へ降りる(画面上部から次カットが出てくる)
51
+ - `reveal-up` = 前カットが上へ抜ける(画面下部から次カットが出てくる)
52
+ - 実測(64x64・10fps・duration 1s・遷移中間 t=2.5s): `reveal-down` で上半分 RGB(0,0,253)=次カット /
53
+ 下半分 RGB(252,0,0)=前カット。`reveal-up` はこの上下が入れ替わる
54
+ - **他の xfade と同じく、遷移の重なり分だけタイムラインが縮む**(境界 1 つにつき `duration` 秒)。
55
+ `layers[]` / `overlays[]` / `audio.sfx[]` を**タイムライン秒で手置き**しているプロジェクトでは、
56
+ トランジションを足すと後続の配置が全部ずれる。字幕は (`src`, source 秒) で書くのでエンジンが
57
+ 追随するが、手置きの要素は自分で引き直す必要がある。尺を変えたくない場合は、
58
+ トランジションではなくオーバーレイで表現する(前カット最終フレームを焼いて動かす)という
59
+ 逃げ道もあるが、静止画になるうえプロジェクト固有の焼き込みが要るので既定にはしない
34
60
 
35
61
  ## 3. 残裁定
36
62
 
@@ -53,6 +79,8 @@
53
79
  - **`tpad` の `start_mode=clone` は使わない**: カット先頭(`at_sec=0`)での静止を素直に `tpad=start_mode=clone:start_duration=X` で実装すると、後続に(本機能の他パスも含め)`fps` フィルタが一つでも挟まると出力の**最終フレームが 1 枚欠落する**バグをこの ffmpeg ビルドで実機検証した(`stop_mode=clone` には同じ問題が無いことも確認済み)。代わりに、`split` で複製した全区間トリムの一方を `trim=start_frame=0:end_frame=1`(フレーム番号ベース・fps に依存しない)で 1 フレームへ切り、`stop_mode=clone` + `stop=<フレーム数-1>`(時間指定の `stop_duration` ではなく整数フレーム数)で伸ばしてから元の全区間へ concat する
54
80
  - **フリーズ中の音声は無音挿入**(direct 音の継続やループはしない): 直前音をループさせるとループ境目でクリックノイズが乗る(PCM の非ゼロ交差での接続)のに対し、無音挿入は決定論的でグリッチが無い。narration/BGM/SFX は出力タイムライン上の絶対秒で独立に配置される既存契約(`cuts[].speed` と同じ前提)のため、freeze による尺の伸びに合わせて自動シフトはしない
55
81
  - **v0 は gap-aware タイムライン(明示 `at`/`track`)との併用不可**: gap-aware パス(`computeVideoRuns`)の出力秒→ソース秒写像は速度係数のみを前提にした線形式で、フリーズによる非線形な静止区間があると破綻する。`cuts[].freeze` が宣言された状態で gap-aware 判定(`needsGapAwareCutTimeline`)が真になる場合、render-cut は明示的に例外を投げて止まる(silent drop を許さない契約の原則どおり、機能を無言で無視しない)。デフォルトの逐次タイムラインでのみ有効
82
+ - **v1(2026-08-18 追記)も同じ制約**: `contract-2026-08-18-v1-render-parity.md` で v1
83
+ (`sources[]`)の `buildMultiSourceCutCommand` にも gap-aware タイムライン(`buildGapAwareMultiSourceCutCommand`)が入った。理由は v0 と全く同じ(`computeVideoRuns` の線形写像がフリーズを表現できない)ため、`cuts[].freeze` + 明示 `at`/`track` の組み合わせは v1 でも同じ例外で止まる
56
84
 
57
85
  ### 4-3. プレビュー乖離
58
86
 
@@ -121,6 +121,13 @@ HEVC alpha MOV は「Apple 系ツールへの受け渡しが要るとき」の
121
121
  がそのまま適用される。`beats` / `emphasis_words` と同じ扱いである
122
122
  - マットに `--ss` 相当のオフセットを持たせない。素材の途中区間だけを切り出したマットを
123
123
  `person_matte` に載せない(載せると時刻 0 の一致が壊れる)
124
+ - 一方で、**カット単位に切り出したマットを `layers[].src` へ直接置く**運用(`analysis.json` を
125
+ 経由しない、プロジェクト固有の `assets/matte/*.mov` など)は本契約の管轄外である。
126
+ その場合は本節の「時刻 0 一致」が成り立たないので、**切り出しの由来(元素材 / in / out /
127
+ speed / fps)を素材の隣に必ず残す**こと。由来が無いと消費側は素材の頭が何の時刻かを
128
+ 推測するしかなく、十数フレーム単位でずれたまま気づけない(2026-08-14 に実害)。
129
+ 運用上の注意は `docs/contract-2026-08-02-preview-parity.md` §2.4 と
130
+ `skills/edit-plan/execution.md`「レイヤー素材の時間基準」を参照
124
131
  - 消費側は表示・書き出しのたびに `cuts[]` から timeline 秒へ射影する。射影結果を永続化しない。
125
132
  同一 source 区間が複数回現れれば、1 本のマットが複数の timeline 位置へ射影される
126
133
  - マットの尺が素材の尺より短い場合、超えた範囲は**マット無し**として扱う(エラーではない)。
@@ -63,3 +63,44 @@
63
63
  - UI: 実機で (a) 配置原則どおりの表示 (b) 音声トラック追加とアイテム移動が edit.json に
64
64
  書き戻る (c) 音源バー端ドラッグで in/out 書き戻り・リロード後保持 (d) トリマーの
65
65
  表示・調整が機能 (e) 既存トラック UI・z 順の無退行
66
+
67
+ ## 5. §2 追記 — sfx フェード(audio-clip-fades, 2026-08-18・オーナー裁定「クリップ主義」T2)
68
+
69
+ BGM をクリップ化する裁定(内部リポ `tasks/2026-08-18-bgm-clip-placement-ruling`)に伴い、
70
+ 「音楽をクリップ(audio.sfx[])として置いても BGM ベッドと同じフェード表現ができる」を
71
+ 満たすため、`sfxItem` に optional の `fade_in` / `fade_out`(秒・0 以上)を追加のみ拡張する
72
+ (`version` 不変・`contract-2026-07-17-data-contract-versioning.md` の原則に従う)。
73
+
74
+ ### schema
75
+
76
+ - `sfxItem.fade_in` / `fade_out`: 秒・省略時 0(フェードなし)。`audio.bgm.fadeIn` /
77
+ `fadeOut`(camelCase)とは異なり **snake_case**(既存の `gain_db` と同じ命名系列)
78
+ - フェード対象はこのクリップの実効再生窓 `[t, t + 実効尺)`。実効尺は §2 の `[in, out)` が
79
+ 既知なら `out − in`、`in`/`out` 省略時は素材尺(消費側が実尺を解決できた場合のみ)
80
+ - クランプ規則は `audio.bgm.fadeIn`/`fadeOut` と同型: `fade_in`/`fade_out` それぞれ独立に
81
+ 実効尺の半分までクランプ(render-cut が実装、edit-lint は `in`/`out` が両方既知のときだけ
82
+ 警告できる — lint は ffprobe を持たないため実尺越えの検知は消費側の責務、という §2 本文の
83
+ 既存原則をフェードにもそのまま適用)
84
+
85
+ ### 消費(render-cut + preview 3 面)
86
+
87
+ - render-cut: sfx の afade を volume の直後・adelay の直前に挿入する(adelay 後だと
88
+ `st=0` が delay 由来の無音区間を指してしまうため)。`in`/`out` 併用時は atrim/asetpts で
89
+ 尺をリセットした後の実効尺基準で afade を計算する
90
+ - シェルプレビュー(akari-preview): sfx は 1 回きりの `BufferSourceNode` 再生のため、
91
+ bgm の毎 tick 再計算(fadeMultiplier)ではなく、schedule 時点で
92
+ `gain.gain.setValueAtTime`/`linearRampToValueAtTime` によるブレークポイント列を組む
93
+ (`sfxFadeGainSchedule`、シーク再開時は経過秒からブレークポイントを再構成)
94
+ - Web UI(preview-server): bgm と同じ毎 tick 再計算方式。ただしこの層は現状 sfx の
95
+ `in`/`out` トリム自体を未実装のため、フェードの実効尺は常にデコード済み素材全長を使う
96
+ (トリム実装時に合わせて見直す)
97
+
98
+ ### インスペクター
99
+
100
+ - akari-annotations: sfx 選択時に bgm と同じ「フェード」タブ(`fadeIn`/`fadeOut` ノブ)を出す。
101
+ ducking は bgm 概念のため sfx には出さない
102
+ - 正本は `packages/edit-store`(edit.json テキスト手術)だが、本追記の実装レーン
103
+ (task 2026-08-18-audio-clip-fades)のファイル境界が `packages/edit-store` を含まないため、
104
+ 書き戻りは `apps/shell/extensions/akari-annotations/src/common/sfx-fade-store.ts` に
105
+ 境界内で完結する独立実装として置いた(`updateArrayElementByIndex` 等 edit-store の
106
+ export 済みユーティリティは再利用)。将来 edit-store 側の担当タスクが正本へ統合してよい
@@ -57,6 +57,23 @@
57
57
  ### 2.4 レイヤー(B-roll)
58
58
  - `t` 〜 `t + duration` の窓外では非表示。**初期状態も非表示**(窓に入るまで描画しない)
59
59
  - 表示中は `currentTime` を出力時刻に同期する
60
+ - **素材内オフセット(in トリム)は無い。素材の先頭が常に `t` に対応する**。`duration` は
61
+ 素材の先頭から何秒使うかであって、素材のどこを使うかは選べない(`cuts[].in/out` に相当する
62
+ ものが `layers[]` には存在しない)。素材の途中区間を重ねたいときは**素材そのものを切り出す**
63
+ 必要がある。
64
+ - 切り出した素材は「何を・どこから・どの速度で切り出したか」が失われるため、
65
+ **由来(元素材 / in / out / speed / fps)を素材の隣に必ず残す**こと。残っていないと、
66
+ 次に触る人(人間・AI とも)が「素材の頭が何の時刻なのか」を推測することになり、
67
+ 十数フレーム単位でズレたまま気づけない。
68
+ - 実害例(2026-08-14・リール制作): カット単位に切り出した人物マットを「先行表示分の
69
+ プリロールを持っているはず」と**推測**して頭をトリムしたところ、実際は切り出し済みで
70
+ プリロールが無く、11〜23 フレームずれた。さらに `duration` を詰めた結果、区間の末尾で
71
+ マットが尽きて「人物が消えて背景だけ」になった。
72
+ - 素材とカットの時間対応を後から実測する場合、**フレーム差分の絶対値(`blend=difference`)は
73
+ 使わない**。色調整(`output.look` は本編にしか掛からない = §2.4 冒頭の別項)で素材と本編の
74
+ 色が違うと、その色差が支配して指標が平坦になり誤った結論を導く。**フレーム間差分エネルギーの
75
+ 時系列(`tblend=all_mode=difference` → `signalstats` の YAVG)を正規化して相互相関**させると、
76
+ 色に不変で lag を特定できる。
60
77
 
61
78
  #### 2.4.1 空間クロップ(`layers[].crop`。2026-08-06 導入)
62
79
  - `crop = { x, y, w, h }`(**0..1 正規化・ソースフレーム相対・静的**)。省略時は既定
@@ -539,7 +556,7 @@ ffmpeg の `perspective` フィルタの制約(式に時刻変数を持たな
539
556
  | `layers[].perspective`(2026-08-06 実装) | ✅(§2.4.4。実ブラウザ実測済み) | ✅(§2.4.4。tsc -b + ユニット + Web 同一計算式で担保) |
540
557
  | `cuts[].fx`(2026-08-07 実装・近似あり) | 🟡(§2.4.5。5 種対応、3 種は近似バッジ付き) | ❌(未実装) |
541
558
  | `layers[].keyframes`(2026-08-09 実装) | ✅(§2.4.7。transform/crop は連続補間。perspective は blend:"normal" のみ・書き出しの段階保持とサンプル点で一致) | ✅(§2.4.7。同左) |
542
- | `cuts[].static-image-source`(2026-08-12 実装。正本: `contract-2026-08-12-still-image-cut-source-v0.md`) | 🟡(`<img>`/`<video>` 出し分け + preview-engine ClipSession/Timeline の image 対応を実装。framing/freeze/transform は流用。実ブラウザでの対話的スクラブ・複数区間切替の実機検証は未実施) | ❌(スコープ外・未対応) |
559
+ | `cuts[].static-image-source`(2026-08-12 実装。正本: `contract-2026-08-12-still-image-cut-source-v0.md`) | 🟡(`<img>`/`<video>` 出し分け + preview-engine ClipSession/Timeline の image 対応を実装。framing/freeze/transform は流用。実ブラウザでの対話的スクラブ・複数区間切替の実機検証は未実施。2026-08-17: 静止画が stylesheet の display:none に隠れたまま永久に出ない実機バグ(`img.style.display=''`)を是正) | 🟡(2026-08-17 実装 — task/2026-08-17-shell-still-image-cut-preview。#preview-still + gap と同じ壁時計クロックで表示。cut transform/framing/freeze/選択ドラッグは video のスタイル鏡写しで流用。タイムラインの静止画フィルムストリップ/サムネも同時是正(probeForFilmstrip の duration 必須ガードが静止画分岐を dead code 化していた)。Electron 実機での対話検証は未実施) |
543
560
 
544
561
  - `cuts[].framing`(静的クロップ / ズームキーフレーム)・`cuts[].freeze`(フリーズ)は
545
562
  `contract-2026-07-22-render-basics.md` #6/#7 としてレンダ(render-cut)に加え、
@@ -72,6 +72,40 @@ before checksum confirmation. Parse, field, process, or 1 MiB capture failures b
72
72
  `MEASUREMENT_ERROR` shape, keep a content-addressed artifact and receipt when structurally possible,
73
73
  leave render state in `phase:error`, return exit 1, and cannot produce an integrity candidate.
74
74
 
75
+ ### 3.1 True peak AAC overshoot guard (2026-08-17, task 2026-08-17-render-cut-true-peak-guard)
76
+
77
+ The `filter_report.normalized.output_tp` loudnorm reports for the PCM stage is not the artifact's
78
+ real true peak: the AAC re-encode that follows can measurably overshoot it (a real render measured
79
+ `filter_report` at -1.00 dBTP against a decoded artifact at +0.23 dBFS — about +1.2 dB of
80
+ codec-introduced overshoot; `planning/notes-2026-08-17-mac-fresh-install-bug-reports.md` #05). Two
81
+ additive mitigations apply only when `true_peak_dbtp` is **explicit** in `audio.master` — the -1.5
82
+ dBTP default is unchanged and unmargined:
83
+
84
+ - **Applied margin.** `packages/render-cut/src/plan.mjs` hands loudnorm `configured -
85
+ AAC_TRUE_PEAK_OVERSHOOT_MARGIN_DBTP` (1.5 dB, `packages/render-cut/src/audio-qc.mjs`) instead of
86
+ the raw configured value, so the *decoded* artifact — not just the PCM stage — has a better chance
87
+ of landing under what the caller asked for. The receipt records both under an additive
88
+ `audio_qc.true_peak_margin: { overshoot_margin_dbtp, applied_true_peak_dbtp }` field;
89
+ `audio_qc.configured.true_peak_dbtp` is unchanged and still reports the caller's original value.
90
+ The margin is a fixed mitigation, not a guarantee — real-render testing found synthetic
91
+ high-transient material where even the margined target still decodes above 0 dBFS (this is what
92
+ the next mitigation exists to catch).
93
+ - **Overshoot detection.** When `decoded_measurement.normalized.input_tp` exceeds
94
+ `configured.true_peak_dbtp` by more than a 0.1 dB tolerance, `buildAudioQc` appends an additive
95
+ `audio_qc.warnings: ["TRUE_PEAK_EXCEEDED: ..."]` entry — readable from the receipt alone, no
96
+ human needs to eyeball the two numbers. `verdict` deliberately **stays `"INCONCLUSIVE"`**, not a
97
+ new value: `packages/akari-launcher/src/status-core/integrity.mjs` (mirrored at
98
+ `plugin/runtime/status-core/integrity.mjs`) closed-world-validates the successful-measurement
99
+ branch and rejects any verdict string other than `"INCONCLUSIVE"` as a structural integrity
100
+ problem, and `accept-command.mjs` keys its human-review warning off that exact string. A new
101
+ verdict value would have misreported a legitimate receipt as malformed instead of surfacing the
102
+ overshoot, so exceeding true peak is additive evidence on an otherwise-`INCONCLUSIVE` receipt, not
103
+ a verdict of its own.
104
+
105
+ Both fields are additive to the existing `configured` / `filter_report` / `decoded_measurement`
106
+ triple — nothing already reading `audio_qc` needs to change, and their absence (when
107
+ `true_peak_dbtp` is left at its default, or when nothing exceeded) is the unchanged legacy shape.
108
+
75
109
  ## 4. Recipe boundary and evidence grade
76
110
 
77
111
  Recipe `caption_style_ref` is descriptive only. It is not registry-backed and never injects caption
@@ -6,6 +6,11 @@ updated: 2026-08-12
6
6
 
7
7
  # 静止画 cut ソース契約 v0
8
8
 
9
+ > **読み替え注記**: 表題の `-v0` は本契約文書自身のリビジョンであり、edit.json の
10
+ > スキーマ版(`version`)とは別物。本契約が対象とするのは edit v0 の `source.path` と
11
+ > edit v1 の `sources[].path` の両方(語彙の区別は
12
+ > [contract-2026-07-17-data-contract-versioning.md](./contract-2026-07-17-data-contract-versioning.md) §5)。
13
+
9
14
  - 日付: 2026-08-12
10
15
  - 状態: **ドラフト**(v0 実装と同時に確定させる。実装で判明した齟齬は追記で解消)
11
16
  - 前提:
@@ -157,6 +162,14 @@ Electron シェル本体(`apps/shell`)のプレビュー対応は本タス
157
162
  image-layer-parity ではシェル側 webview も同時対応していたが、本タスクの司令塔裁定でシェルは
158
163
  別タスクへ切り出されている。§3 の適合状況表にシェル列は `❌`(未対応)として記録する。
159
164
 
165
+ **追記(2026-08-17)**: 切り出されていたシェル対応を task/2026-08-17-shell-still-image-cut-preview
166
+ で実装した。方式は Web UI(§5.2)と同型 — `#preview-still`(`<img>`)を `#preview-video` に重ね、
167
+ 静止画セグメントのクロックは gap セグメントと同じ壁時計原点を共用する。カットの
168
+ transform / framing / 選択ドラッグは video 要素のインラインスタイルを毎フレーム鏡写しにする
169
+ ことで既存レールをそのまま流用。タイムライン(akari-annotations)の静止画フィルムストリップも
170
+ 同タスクで是正(`probeForFilmstrip` の duration>0 必須ガードが、ffprobe が duration を報告しない
171
+ 静止画〔§2.3〕で既存の isImage 分岐を dead code 化していた)。適合状況はパリティ契約 §3 を参照。
172
+
160
173
  ## 6. 適合状況の更新
161
174
 
162
175
  `contract-2026-08-02-preview-parity.md` §3 の適合状況表に `cuts[].static-image-source` 行を