akari-video 0.1.11 → 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 (78) 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 +2 -0
  9. package/vendor/docs/contract-2026-07-25-r6-audio-tracks-and-trim.md +41 -0
  10. package/vendor/docs/contract-2026-08-02-preview-parity.md +1 -1
  11. package/vendor/docs/contract-2026-08-03-caption-display-encoding-qc-v1.md +34 -0
  12. package/vendor/docs/contract-2026-08-12-still-image-cut-source-v0.md +13 -0
  13. package/vendor/docs/contract-2026-08-18-v1-render-parity.md +115 -0
  14. package/vendor/packages/akari-launcher/package.json +2 -2
  15. package/vendor/packages/audio-library-setup/README.md +2 -2
  16. package/vendor/packages/audio-library-setup/bin/beat-grid.mjs +20 -12
  17. package/vendor/packages/audio-library-setup/shared/beat-grid.mjs +60 -11
  18. package/vendor/packages/audio-library-setup/test/beat-grid.test.mjs +58 -24
  19. package/vendor/packages/edit-lint/src/edit-lint.mjs +382 -306
  20. package/vendor/packages/edit-store/lib/caption-display.js +4 -2
  21. package/vendor/packages/edit-store/lib/edit-store.d.ts +11 -21
  22. package/vendor/packages/edit-store/lib/edit-store.js +25 -531
  23. package/vendor/packages/edit-store/lib/edit-v2.d.ts +147 -0
  24. package/vendor/packages/edit-store/lib/edit-v2.js +296 -0
  25. package/vendor/packages/edit-store/lib/index.d.ts +6 -0
  26. package/vendor/packages/edit-store/lib/index.js +12 -0
  27. package/vendor/packages/edit-store/lib/internal-model.d.ts +183 -0
  28. package/vendor/packages/edit-store/lib/internal-model.js +492 -0
  29. package/vendor/packages/edit-store/lib/migrate/error.d.ts +4 -0
  30. package/vendor/packages/edit-store/lib/migrate/error.js +13 -0
  31. package/vendor/packages/edit-store/lib/migrate/index.d.ts +53 -0
  32. package/vendor/packages/edit-store/lib/migrate/index.js +404 -0
  33. package/vendor/packages/edit-store/lib/migrate/legacy-parse.d.ts +31 -0
  34. package/vendor/packages/edit-store/lib/migrate/legacy-parse.js +552 -0
  35. package/vendor/packages/edit-store/lib/retime.d.ts +9 -0
  36. package/vendor/packages/edit-store/lib/retime.js +79 -0
  37. package/vendor/packages/edit-store/lib/timeline-map.d.ts +1 -8
  38. package/vendor/packages/edit-store/lib/timeline-map.js +1 -48
  39. package/vendor/packages/edit-store/lib/track-order.d.ts +38 -0
  40. package/vendor/packages/edit-store/lib/track-order.js +63 -0
  41. package/vendor/packages/edit-store/lib/webview-kernel.js +0 -37
  42. package/vendor/packages/edit-store/lib/write-gate.d.ts +41 -15
  43. package/vendor/packages/edit-store/lib/write-gate.js +91 -70
  44. package/vendor/packages/media-bin/package.json +2 -1
  45. package/vendor/packages/media-bin/scripts/build-whisper.mjs +224 -0
  46. package/vendor/packages/media-bin/scripts/fetch-binaries.mjs +22 -1
  47. package/vendor/packages/media-bin/src/binary-manifest.mjs +60 -3
  48. package/vendor/packages/media-bin/src/index.mjs +71 -17
  49. package/vendor/packages/media-bin/test/whisper.test.mjs +142 -0
  50. package/vendor/packages/project-scaffold/src/index.mjs +10 -1
  51. package/vendor/packages/project-scaffold/test/create-project.test.mjs +10 -2
  52. package/vendor/packages/schemas/bin/validate-edit.mjs +12 -1
  53. package/vendor/packages/schemas/edit.schema.json +264 -5
  54. package/vendor/packages/schemas/examples/edit-sfx-fade-invalid/edit.json +10 -0
  55. package/vendor/packages/schemas/examples/edit-sfx-in-out-valid/edit.json +1 -1
  56. package/vendor/packages/schemas/examples/edit-v2-fractional-fps-invalid/edit.json +6 -0
  57. package/vendor/packages/schemas/examples/edit-v2-html-in-out-invalid/edit.json +19 -0
  58. package/vendor/packages/schemas/examples/edit-v2-item-kind-field-invalid/edit.json +20 -0
  59. package/vendor/packages/schemas/examples/edit-v2-items-content-invalid/edit.json +13 -0
  60. package/vendor/packages/schemas/examples/edit-v2-minimal-valid/edit.json +6 -0
  61. package/vendor/packages/schemas/examples/edit-v2-telop-in-out-invalid/edit.json +19 -0
  62. package/vendor/packages/schemas/examples/edit-v2-valid/edit.json +86 -0
  63. package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +78 -0
  64. package/vendor/packages/schemas/test/validate-edit.test.mjs +17 -0
  65. package/vendor/skills/analyze-footage/media-and-transcript.md +15 -10
  66. package/vendor/skills/edit-plan/SKILL.md +13 -11
  67. package/vendor/skills/edit-plan/approvals-and-generation.md +2 -2
  68. package/vendor/skills/edit-plan/beat-sync.md +52 -104
  69. package/vendor/skills/edit-plan/beats.md +11 -9
  70. package/vendor/skills/edit-plan/emphasis-detection.md +11 -44
  71. package/vendor/skills/edit-plan/execution.md +75 -93
  72. package/vendor/skills/edit-plan/report-guide.md +4 -4
  73. package/vendor/skills/edit-plan/workflow.md +1 -1
  74. package/vendor/skills/export-nle/SKILL.md +26 -6
  75. package/vendor/skills/overlay-authoring/SKILL.md +13 -1
  76. package/vendor/skills/setup-library/SKILL.md +1 -1
  77. package/vendor/skills/setup-library/tools-check.md +74 -17
  78. 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.11",
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>` で変換してから開いてください。
@@ -79,6 +79,8 @@
79
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 する
80
80
  - **フリーズ中の音声は無音挿入**(direct 音の継続やループはしない): 直前音をループさせるとループ境目でクリックノイズが乗る(PCM の非ゼロ交差での接続)のに対し、無音挿入は決定論的でグリッチが無い。narration/BGM/SFX は出力タイムライン上の絶対秒で独立に配置される既存契約(`cuts[].speed` と同じ前提)のため、freeze による尺の伸びに合わせて自動シフトはしない
81
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 でも同じ例外で止まる
82
84
 
83
85
  ### 4-3. プレビュー乖離
84
86
 
@@ -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 側の担当タスクが正本へ統合してよい
@@ -556,7 +556,7 @@ ffmpeg の `perspective` フィルタの制約(式に時刻変数を持たな
556
556
  | `layers[].perspective`(2026-08-06 実装) | ✅(§2.4.4。実ブラウザ実測済み) | ✅(§2.4.4。tsc -b + ユニット + Web 同一計算式で担保) |
557
557
  | `cuts[].fx`(2026-08-07 実装・近似あり) | 🟡(§2.4.5。5 種対応、3 種は近似バッジ付き) | ❌(未実装) |
558
558
  | `layers[].keyframes`(2026-08-09 実装) | ✅(§2.4.7。transform/crop は連続補間。perspective は blend:"normal" のみ・書き出しの段階保持とサンプル点で一致) | ✅(§2.4.7。同左) |
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 は流用。実ブラウザでの対話的スクラブ・複数区間切替の実機検証は未実施) | ❌(スコープ外・未対応) |
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 実機での対話検証は未実施) |
560
560
 
561
561
  - `cuts[].framing`(静的クロップ / ズームキーフレーム)・`cuts[].freeze`(フリーズ)は
562
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` 行を
@@ -0,0 +1,115 @@
1
+ ---
2
+ lifecycle: draft
3
+ created: 2026-08-18
4
+ updated: 2026-08-18
5
+ ---
6
+
7
+ # v1 レンダー経路パリティ契約 — cuts[].at / cuts[].track
8
+
9
+ - 日付: 2026-08-18
10
+ - 状態: **ドラフト**(実装と同時に確定させる)
11
+ - 前提:
12
+ - `contract-2026-07-22-render-basics.md`(`cuts[].freeze` とゲートアウェア・タイムラインの
13
+ 非併用制約。本契約はその制約を v1 の等価経路へそのまま延長する)
14
+ - `contract-2026-08-12-still-image-cut-source-v0.md`(`buildMultiSourceCutCommand` の
15
+ 静止画 `-loop 1` レシピ。本契約が追加する経路もこのレシピをそのまま継承する)
16
+ - スコープ: `packages/render-cut/src/plan.mjs`(cut コマンド生成)/
17
+ `packages/edit-lint/src/edit-lint.mjs`(警告の整合)
18
+
19
+ ## 0. 背景・実害
20
+
21
+ v0(`source` 単一)の `cuts[]` は `at`(明示配置・ギャップ)と `track`(多段合成)を
22
+ `buildGapAwareCutCommand` / `buildTrackStackPlan` で解釈していたが、v1(`sources[]`)の
23
+ `buildMultiSourceCutCommand` は `cuts[]` を配列順に**連結するだけ**で `at`/`track` を一切見て
24
+ いなかった。UI のドラッグ操作は v1 プロジェクトへ `at`/`track` を普通に書くため、プレビュー
25
+ (kernel は `at`/`track` を正しく写像)と書き出しが食い違う WYSIWYG 破綻が実機で発生した
26
+ (2026-08-18)。`track` >= 1 のカットは合成されず出力尺へそのまま連結され、一度も画面に出ない
27
+ まま尺だけ伸びた mp4 が焼き上がっていた。
28
+
29
+ ## 1. 裁定 — どの dispatch を直すか
30
+
31
+ v1 で `at`/`track` が実際に効く経路は 2 つあり、**症状が出ていたのは片方だけ**だった:
32
+
33
+ 1. **既定順(`usesDefaultTrackOrder` が true)**: `buildPlan` が `buildMultiSourceCutCommand` を
34
+ `edit.cuts` へ直接呼ぶ。UI が書く典型(`timeline.tracks` を明示宣言しない)はこちら。
35
+ **ここが壊れていた** — `at`/`track` を一切見ない単純連結だった。
36
+ 2. **カスタム順(`timeline.tracks` が既定と異なる並びを明示宣言)**: `buildTrackStackPlan` が
37
+ `track` ごとに `cuts[]` をフィルタし、`buildMultiSourceCutCommand`(フィルタ後の配列。素の
38
+ 逐次連結のまま)→ `resolveCutTrackRanges`(`track-compose.mjs`)が「素の逐次連結クリップの
39
+ どこに各カットの中身があるか」を `offsets` 累積和で追跡し、`buildCutTrackCompositeCommand`
40
+ の `overlay=...enable=...` で `at` 位置へ配置する、という**補正込みの設計で最初から正しく
41
+ 動いていた**(`track-compose.test.mjs` の実レンダーテストで確認済み・pixel 検証あり)。
42
+
43
+ よって本タスクは **(1) の dispatch だけを直す**。`buildMultiSourceCutCommand` 自体・
44
+ `buildTrackStackPlan`・`resolveCutTrackRanges` は変更しない — (2) の補正ロジックは
45
+ `buildMultiSourceCutCommand` が「常に素の逐次連結を返す」ことに依存しており、(1) の dispatch
46
+ 内部で分岐を追加すると (2) を壊す(`track` >= 1 のフィルタ済み配列は常に
47
+ `needsGapAwareCutTimeline` が真になるため、素の分岐と衝突する)。
48
+
49
+ ## 2. 実装
50
+
51
+ ### 2.1 新規関数 `buildGapAwareMultiSourceCutCommand`(`plan.mjs`)
52
+
53
+ v0 の `buildGapAwareCutCommand` の v1 版。`buildPlan` の v1 分岐だけが呼ぶ
54
+ (`buildTrackStackPlan` の per-track 呼び出しは既存の `buildMultiSourceCutCommand` のまま)。
55
+
56
+ ```
57
+ cut = needsGapAwareCutTimeline(edit.cuts)
58
+ ? buildGapAwareMultiSourceCutCommand(...) // at ギャップ or track>=1 が宣言されている
59
+ : buildMultiSourceCutCommand(...) // 既存の単純連結(無改修)
60
+ ```
61
+
62
+ - **ギャップの埋め方**: v0 と同一(`color=c=black:...` で尺ぶん黒塗り)。`look`(LUT)は
63
+ 無視した素の黒 — v0 の `buildGapAwareCutCommand` と同じ挙動
64
+ - **多段合成の意味論**: `computeVideoRuns`(`cut-timeline.mjs`)の winner-take-all スイッチ。
65
+ ある瞬間に最も高い `track` 番号のカットが**画面全体を占有**する(同時アルファ合成ではない)
66
+ — v0 が既定順で行っているのと全く同じモデルで、新しいリッチな合成モデルではない
67
+ - **音声はカット単位(ラン単位ではない)**: 各カットの `[in,out)` がそれぞれの `at` 位置で
68
+ 再生され、`amix` で重ね合わさる。画面はどちらか一方しか映らなくても、音声は両方鳴る
69
+ (v0 の `buildGapAwareCutCommand` の音声ループをそのまま踏襲)
70
+ - **`cuts[].freeze` との非併用**: v0 と同じ理由(`computeVideoRuns` の出力秒→ソース秒写像が
71
+ speed のみを前提にした線形式で、フリーズの非線形な静止区間を表現できない)で、
72
+ `hasCutFreeze(cuts)` なら例外を投げて止める(silent drop しない)
73
+ - **静止画 `-loop 1`・`transform`(scale/x/y/rotate)・`fx`・LUT**: 既存の
74
+ `appendCutVisualTransform` / `appendCutFxChain` / `isImageLayerSource` をそのまま再利用
75
+
76
+ ### 2.2 `predictedDuration` の並び替え
77
+
78
+ v1 は従来 `version === 1` の分岐が最初に来ており、`needsGapAwareCutTimeline` の判定に
79
+ 一度も到達しなかった(= at ギャップ・PiP を考慮しない `sequentialDurationWithTransitionOverlap`
80
+ の単純合計を常に返していた)。ギャップアウェア判定をバージョン分岐より前に出し、v0/v1 共通で
81
+ 「セグメント終端の最大値」(`resolveCutSegments` の `end` の max)を使うよう修正。これにより
82
+ `verify.duration` が新しい(正しい)レンダー尺と整合する。
83
+
84
+ ### 2.3 `cut_track_declaration_unrendered` フラグの撤去
85
+
86
+ `buildPlan` が返していた「v1 の track/at 宣言が効いていない」ヒント旗(`verifyArtifact` が
87
+ `verify.duration` 失敗時に付加していた注記)は、根本原因が解消されたため撤去。誤解を招く
88
+ 古い注記を残さない。
89
+
90
+ ## 3. edit-lint の警告撤去
91
+
92
+ `cuts.track-render-unsupported` / `cuts.at-render-unsupported`
93
+ (`packages/edit-lint/src/edit-lint.mjs` の `validateCutTrackRenderSupport`)は
94
+ 「v1 の書き出しは track/at を無視する」ことを警告するためだけに存在していた。2.1 の実装で
95
+ 根本原因が解消されたため、チェックごと撤去した。`cuts.track-transition-unsupported`
96
+ (カスタム `timeline.tracks` 順での `transition_out` 非対応)は本契約の対象外 — §1 の (2) の
97
+ 経路自体は変更していないため、既存のまま有効。
98
+
99
+ ## 4. 既知の制約(v0 と同一 — 新規に増やしていない)
100
+
101
+ - `cuts[].freeze` と at/track ギャップアウェア・タイムラインの併用は不可(§2.1)。lint での
102
+ 事前警告はない(v0 も render 時の例外のみで、lint チェックは元から無い。同じ姿勢を踏襲)
103
+ - 多段合成は同時アルファオーバーレイではなく winner-take-all スイッチ(§2.1)。カスタム
104
+ `timeline.tracks` 順(§1 の (2))を使えば `buildTrackStackPlan` 経由の本当の重ね合わせ
105
+ (`overlay=...enable=...`)になる — 使い分けは v0 と同じ
106
+
107
+ ## 5. 検証
108
+
109
+ - L0: `packages/render-cut` / `packages/edit-lint` の `npm test` 全 PASS
110
+ - 実 ffmpeg(`packages/render-cut/test/v1-track-parity.test.mjs`):
111
+ (a) at ギャップ入り v1 プロジェクトの出力尺・各カットの出現時刻が宣言どおり(±1 フレーム)
112
+ (b) `track:1` の PiP が実際に画面に合成される(PiP 領域の画素が下段と異なる)
113
+ (c) 既存 v1 プロジェクト(at 無し・連結)の非回帰(フィルタグラフが従来のまま byte 一致)
114
+ 加えて、オーナーの実プロジェクト相当(v1・全静止画・at ギャップ・音声クリップ)の fixture で
115
+ 書き出し尺とタイミングがタイムライン宣言と一致することを確認
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.11",
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 組み込みモジュールのみ)。 [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": [
@@ -34,7 +34,7 @@
34
34
  "scripts": {
35
35
  "prepack": "node scripts/prepack.mjs",
36
36
  "postpack": "node scripts/prepack.mjs clean",
37
- "test": "node --test test/*.mjs"
37
+ "test": "node --test --test-concurrency=1 test/*.mjs"
38
38
  },
39
39
  "akariVideoVendor": {
40
40
  "execution": "reference-only",
@@ -15,8 +15,8 @@
15
15
  | `bin/fetch-akari-sounds.mjs` | AKARI Sounds を GitHub Release から**一括取得**し user スコープへ登録する CLI(first-party のみ許可。取得先は AkariLabs/akari-sounds に限定) |
16
16
  | `shared/bgm-suggest.mjs` | BGM 自動提案の純粋ロジック — tone 語彙(表現選定と同じ 8 語)× 系統対応表 `FAMILY_TONE_RULES` × 体感 BPM で決定論ランキング |
17
17
  | `bin/suggest-bgm.mjs` | BGM 自動提案 CLI。導入済みスナップショット(`.origin-catalog.json`)を読み、`--tone`(複数可)`--tempo` から候補 + ローカル実体パスを提示(`--json` あり)。`--declarations`(または env `AKARI_SOUNDS_DECLARATIONS`)で耳検証済み宣言を合流 — 実測 BPM 置換・耳検証ボーナス・**サビ頭出し(`audio.bgm.in` の推奨値)**・構成表示が付く。ネットワーク不使用 |
18
- | `shared/beat-grid.mjs` | 宣言(bpm / 頭拍 / キメ / 構成)を **timeline 秒**へ写す純粋ロジック。`audio.bgm.in` とループ(**1 周目は in から・2 周目以降はファイル先頭から**。2026-08-04 に ffmpeg 実測で確定)を反映し、スナップ(キメ > 小節頭 > 拍)とカット候補を返す |
19
- | `bin/beat-grid.mjs` | 音楽グリッド CLI(`--edit` / `--track` + `--timeline`・`--snap`・`--json`)。edit-plan の [beat-sync](../../skills/edit-plan/beat-sync.md) が発火位置を拍へ寄せるのに使う |
18
+ | `shared/beat-grid.mjs` | 宣言(bpm / 頭拍 / キメ / 構成)を timeline へ写す純粋ロジック。`musicGrid()` の秒グリッドは検査用の内部計算として維持し、`toFrameGrid()` / `snapToGrid()` / `cutCandidates()` が出力 fps 上の整数フレームを返す。`audio.bgm.in` とループ(**1 周目は in から・2 周目以降はファイル先頭から**。2026-08-04 に ffmpeg 実測で確定)を反映する |
19
+ | `bin/beat-grid.mjs` | 音楽グリッド CLI(`--edit` / `--track` + `--timeline`・`--fps`・`--snap`・`--json`)。`--edit` では `output.fps` を使い、拍・小節頭・キメ・スナップ・カット候補を整数フレームで返す。edit-plan の [beat-sync](../../skills/edit-plan/beat-sync.md) が発火位置を拍へ寄せるのに使う |
20
20
  | `shared/sfx-suggest.mjs` | SFX / ジングル自動提案の純粋ロジック — 「場面の意味」14 語 × 宣言表 `MEANING_RULES`(候補順 = 優先順・外部補完の参照つき) |
21
21
  | `bin/suggest-sfx.mjs` | SFX / ジングル自動提案 CLI(`--meaning` / `--list` / `--json`)。suggest-bgm の姉妹 |
22
22
  | `bin/review-sfx-mapping.mjs` | 「意味 → 音」対応表の**耳レビュー面**を生成(全意味 × 候補の試聴プレイヤー + 判定 JSON 書き出し。既定出力 `~/.akari/reviews/sfx-mapping.html`) |