akari-video 0.1.4 → 0.1.6

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 (118) hide show
  1. package/README.md +4 -1
  2. package/bin/akari.mjs +13 -4
  3. package/package.json +1 -1
  4. package/src/assets-command.mjs +42 -0
  5. package/src/internal-command.mjs +52 -0
  6. package/src/messages.mjs +6 -1
  7. package/{vendor/skills/generate-narration/bin/generate-narration.mjs → src/narration-command.mjs} +48 -31
  8. package/src/new-command.mjs +119 -0
  9. package/src/project-state.mjs +10 -1
  10. package/src/repo-assets.mjs +20 -4
  11. package/src/status-command.mjs +18 -1
  12. package/src/status-core/display-name.mjs +22 -0
  13. package/src/status-core/status.mjs +9 -1
  14. package/src/status-core/workspace.mjs +123 -0
  15. package/vendor/.akari-capability-sources.json +6 -0
  16. package/vendor/docs/contract-2026-07-14-edit-json-v1-crop.md +8 -0
  17. package/vendor/docs/contract-2026-07-22-render-basics.md +40 -3
  18. package/vendor/docs/contract-2026-07-23-analysis-person-matte.md +1 -1
  19. package/vendor/docs/contract-2026-08-02-creator-root-v1.md +15 -2
  20. package/vendor/docs/contract-2026-08-02-preview-parity.md +458 -1
  21. package/vendor/docs/contract-2026-08-03-caption-display-encoding-qc-v1.md +21 -0
  22. package/vendor/docs/contract-2026-08-05-fx-v0.md +89 -0
  23. package/vendor/docs/contract-2026-08-06-direction-recipes-v0.md +280 -0
  24. package/vendor/packages/akari-launcher/README.md +4 -1
  25. package/vendor/packages/akari-launcher/bin/akari.mjs +13 -4
  26. package/vendor/packages/akari-launcher/package.json +1 -1
  27. package/vendor/packages/akari-tools/package.json +16 -0
  28. package/vendor/packages/asset-resolver/README.md +30 -2
  29. package/vendor/packages/asset-resolver/browse/app.js +124 -0
  30. package/vendor/packages/asset-resolver/browse/index.html +74 -0
  31. package/vendor/packages/asset-resolver/src/browse-server.mjs +151 -0
  32. package/vendor/packages/asset-resolver/src/catalog.mjs +58 -0
  33. package/vendor/packages/asset-resolver/src/entitlements.mjs +36 -0
  34. package/vendor/packages/asset-resolver/src/env.mjs +92 -0
  35. package/vendor/packages/asset-resolver/src/errors.mjs +10 -0
  36. package/vendor/packages/asset-resolver/src/fetch-file.mjs +60 -0
  37. package/vendor/packages/asset-resolver/src/hash.mjs +15 -0
  38. package/vendor/packages/asset-resolver/src/library.mjs +38 -0
  39. package/vendor/packages/asset-resolver/src/paid-zip.mjs +116 -0
  40. package/vendor/packages/asset-resolver/src/resolve.mjs +226 -0
  41. package/vendor/packages/asset-resolver/src/state.mjs +34 -0
  42. package/vendor/packages/asset-resolver/test/browse-server.test.mjs +42 -0
  43. package/vendor/packages/asset-resolver/test/catalog-and-state.test.mjs +37 -0
  44. package/vendor/packages/asset-resolver/test/entitlements.test.mjs +56 -0
  45. package/vendor/packages/asset-resolver/test/fixtures/build-fixture-library.mjs +106 -0
  46. package/vendor/packages/asset-resolver/test/helpers.mjs +28 -0
  47. package/vendor/packages/asset-resolver/test/resolve-integrity.test.mjs +32 -0
  48. package/vendor/packages/asset-resolver/test/resolve-locked.test.mjs +45 -0
  49. package/vendor/packages/asset-resolver/test/resolve-paid-zip.test.mjs +219 -0
  50. package/vendor/packages/asset-resolver/test/resolve-project-copy-darwin.test.mjs +84 -0
  51. package/vendor/packages/asset-resolver/test/resolve-success.test.mjs +55 -0
  52. package/vendor/packages/creator-root/src/index.mjs +238 -2
  53. package/vendor/packages/creator-root/test/create-creator-root.test.mjs +31 -1
  54. package/vendor/packages/creator-root/test/default-root-path.test.mjs +5 -5
  55. package/vendor/packages/direction/README.md +86 -0
  56. package/vendor/packages/direction/bin/expand-direction.mjs +193 -0
  57. package/vendor/packages/direction/package.json +13 -0
  58. package/vendor/packages/overlay-runtime/README.md +22 -2
  59. package/vendor/packages/overlay-runtime/package.json +1 -1
  60. package/vendor/packages/preview-server/src/server.mjs +134 -17
  61. package/vendor/packages/project-scaffold/src/index.mjs +5 -1
  62. package/vendor/packages/project-scaffold/test/create-project.test.mjs +22 -0
  63. package/vendor/packages/render-cut/bin/akari-apply-textstyle.mjs +3 -3
  64. package/vendor/packages/schemas/asset-meta.schema.json +39 -0
  65. package/vendor/packages/schemas/bin/validate-asset.mjs +48 -3
  66. package/vendor/packages/schemas/bin/validate-captions.mjs +149 -5
  67. package/vendor/packages/schemas/bin/validate-edit.mjs +294 -0
  68. package/vendor/packages/schemas/bin/validate-intake.mjs +11 -1
  69. package/vendor/packages/schemas/captions.schema.json +148 -3
  70. package/vendor/packages/schemas/edit.schema.json +155 -7
  71. package/vendor/packages/schemas/examples/edit-cuts-fx-color-overlay-missing-color-invalid/edit.json +9 -0
  72. package/vendor/packages/schemas/examples/edit-cuts-fx-intensity-out-of-range-invalid/edit.json +9 -0
  73. package/vendor/packages/schemas/examples/edit-cuts-fx-invalid-id/edit.json +9 -0
  74. package/vendor/packages/schemas/examples/edit-cuts-fx-unknown-key-invalid/edit.json +9 -0
  75. package/vendor/packages/schemas/examples/edit-cuts-fx-valid/edit.json +19 -0
  76. package/vendor/packages/schemas/examples/edit-layers-invalid-crop-out-of-bounds/edit.json +18 -0
  77. package/vendor/packages/schemas/examples/edit-layers-invalid-perspective-degenerate/edit.json +18 -0
  78. package/vendor/packages/schemas/examples/edit-layers-invalid-perspective-out-of-range/edit.json +18 -0
  79. package/vendor/packages/schemas/examples/edit-layers-invalid-perspective-wrong-corner-count/edit.json +18 -0
  80. package/vendor/packages/schemas/examples/edit-layers-keyframes-valid/edit.json +38 -0
  81. package/vendor/packages/schemas/examples/edit-layers-valid/edit.json +2 -0
  82. package/vendor/packages/schemas/fixtures/intake/invalid-title-wrong-type/intake.json +9 -0
  83. package/vendor/packages/schemas/fixtures/intake/valid-with-title/intake.json +9 -0
  84. package/vendor/packages/schemas/intake.schema.json +8 -0
  85. package/vendor/packages/schemas/test/captions-animation-contract.test.mjs +38 -0
  86. package/vendor/packages/schemas/test/validate-captions.test.mjs +13 -0
  87. package/vendor/packages/schemas/test/validate-edit.test.mjs +355 -0
  88. package/vendor/packages/schemas/test/validate-intake.test.mjs +17 -0
  89. package/vendor/skills/analyze-footage/bin/is-main-module.mjs +11 -0
  90. package/vendor/skills/analyze-footage/bin/person-matte/alpha-tag.mjs +4 -0
  91. package/vendor/skills/analyze-footage/bin/person-matte/person-matte.mjs +5 -2
  92. package/vendor/skills/analyze-footage/bin/transcribe-cloud.mjs +2 -3
  93. package/vendor/skills/analyze-footage/test/entrypoint-symlink.test.mjs +36 -0
  94. package/vendor/skills/analyze-footage/test/person-matte-alpha-tag.test.mjs +21 -0
  95. package/vendor/skills/beat-sync-edit/SKILL.md +3 -3
  96. package/vendor/skills/beat-sync-edit/generator.md +2 -2
  97. package/vendor/skills/beat-sync-edit/templates/gen-timeline.mjs +1 -1
  98. package/vendor/skills/beat-sync-edit/verify-loop.md +3 -2
  99. package/vendor/skills/create-project/SKILL.md +2 -2
  100. package/vendor/skills/edit-plan/expression-selection.md +4 -0
  101. package/vendor/skills/generate-narration/avatar-voice.md +3 -3
  102. package/vendor/skills/generate-narration/engines.md +2 -2
  103. package/vendor/skills/generate-narration/voice-profile-setup.md +2 -2
  104. package/vendor/skills/manage-connections/SKILL.md +9 -5
  105. package/vendor/skills/manage-connections/bin/doctor.mjs +114 -3
  106. package/vendor/skills/manage-connections/bin/resolve-connections.mjs +348 -0
  107. package/vendor/skills/manage-connections/test/resolve-connections.test.mjs +151 -0
  108. package/vendor/skills/overlay-authoring/3d.md +56 -0
  109. package/vendor/skills/overlay-authoring/motion.md +21 -0
  110. package/vendor/skills/overlay-authoring/telop.md +23 -0
  111. package/vendor/skills/setup-library/SKILL.md +12 -0
  112. package/vendor/templates/project-default/.akari/connections.json +1 -225
  113. package/vendor/templates/project-default/AGENTS.md +8 -0
  114. package/vendor/templates/project-default/CLAUDE.md +6 -0
  115. package/vendor/skills/beat-sync-edit/bin/beatmap.mjs +0 -131
  116. package/vendor/skills/beat-sync-edit/bin/probe-frame.mjs +0 -96
  117. package/vendor/skills/beat-sync-edit/bin/render-when-idle.sh +0 -46
  118. package/vendor/skills/create-project/bin/create-project.mjs +0 -88
package/README.md CHANGED
@@ -32,6 +32,9 @@ akari
32
32
  ```
33
33
 
34
34
  サブコマンド: `akari update`(更新確認)/ `akari init`(作業場の作成・確認のみ)/
35
+ `akari new <target-dir> [--template <path>]`(新規プロジェクト作成)/
36
+ `akari narration generate ...`(VOICEVOX / fal-qwen3 ナレーション生成)/
37
+ `akari internal beat-sync-<beatmap|probe-frame|render-when-idle> ...`(beat-sync-edit 内部実行物)/
35
38
  `akari sounds [--variant wav] [--force]`(公式音源の一括ダウンロード。プロンプトなし・headless 可)/
36
39
  `akari store <connect|status|download|disconnect>`(AKARI Store 連携。マイページで発行した
37
40
  接続トークンを `~/.akari/store-credentials.json`(0600)に保存し、購入済み一覧の確認と
@@ -67,7 +70,7 @@ AKARI Video は「同じファイル契約(`.akari/` 配下の JSON)に収
67
70
  | アプリ | 接続ボタン(AKARI Video アプリ) | アプリの「はじめる」画面から接続 → はじめかた選択 |
68
71
 
69
72
  3 つとも最終的に同じもの(`.akari/connections.json` / `.akari/intake.json` /
70
- `skills/create-project`)を読み書きするため、どの入口から始めても続きは他の入口から
73
+ `akari new` が使う共通 project-scaffold)を読み書きするため、どの入口から始めても続きは他の入口から
71
74
  再開できる。
72
75
 
73
76
  ## インストール
package/bin/akari.mjs CHANGED
@@ -1,25 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
  import { run, runUpdateCommand } from '../src/cli.mjs';
3
3
  import { runInitCommand } from '../src/init-command.mjs';
4
+ import { runNewCommand } from '../src/new-command.mjs';
5
+ import { runNarrationCommand } from '../src/narration-command.mjs';
6
+ import { runInternalCommand } from '../src/internal-command.mjs';
4
7
  import { runSoundsCommand } from '../src/sounds-setup.mjs';
5
8
  import { runStatusCommand } from '../src/status-command.mjs';
6
9
  import { runAcceptCommand } from '../src/accept-command.mjs';
7
10
  import { runCapabilityCommand } from '../src/capability-command.mjs';
8
11
  import { runStoreCommand } from '../src/store-command.mjs';
12
+ import { runAssetsCommand } from '../src/assets-command.mjs';
9
13
 
10
14
  const argv = process.argv.slice(2);
11
- // `akari update` / `akari init` / `akari sounds` / `akari status` / `akari accept` /
12
- // `akari capability` / `akari store` は claude へ転送せず、
13
- // 専用のサブコマンドとして扱う(契約 §4-1 / タスク契約 launcher-init(内部リポ)/
14
- // 音源カタログ既定化のオーナー裁定 2026-08-03 / AKARI Store 連携)。
15
+ // `akari update` / `akari init` / `akari new` / `akari narration` / `akari internal` /
16
+ // `akari sounds` / `akari status` / `akari accept` / `akari capability` / `akari store` /
17
+ // `akari assets` は claude へ転送せず、専用のサブコマンドとして扱う(契約 §4-1 /
18
+ // タスク契約 launcher-init(内部リポ)/ 音源カタログ既定化のオーナー裁定 2026-08-03 /
19
+ // AKARI Store 連携 / タスク契約 2026-08-09-agent-assets-discovery)。
15
20
  // それ以外の引数はすべて従来どおり claude へ転送する。
16
21
  const invoke = argv[0] === 'update' ? runUpdateCommand(argv.slice(1))
17
22
  : argv[0] === 'init' ? runInitCommand(argv.slice(1))
23
+ : argv[0] === 'new' ? runNewCommand(argv.slice(1))
24
+ : argv[0] === 'narration' ? runNarrationCommand(argv.slice(1))
25
+ : argv[0] === 'internal' ? runInternalCommand(argv.slice(1))
18
26
  : argv[0] === 'sounds' ? runSoundsCommand(argv.slice(1))
19
27
  : argv[0] === 'status' ? runStatusCommand(argv.slice(1))
20
28
  : argv[0] === 'accept' ? runAcceptCommand(argv.slice(1))
21
29
  : argv[0] === 'capability' ? runCapabilityCommand(argv.slice(1))
22
30
  : argv[0] === 'store' ? runStoreCommand(argv.slice(1))
31
+ : argv[0] === 'assets' ? runAssetsCommand(argv.slice(1))
23
32
  : run(argv);
24
33
 
25
34
  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.4",
3
+ "version": "0.1.6",
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": {
@@ -0,0 +1,42 @@
1
+ import { spawnSync } from 'node:child_process';
2
+
3
+ import { resolveLauncherAssets } from './repo-assets.mjs';
4
+ import { assetsResolverUnavailableError } from './messages.mjs';
5
+
6
+ /**
7
+ * `akari assets <list|fetch|sync|...>` — `packages/asset-resolver` の CLI
8
+ * (`bin/akari-assets.mjs`)への薄い委譲(タスク契約 2026-08-09-agent-assets-discovery)。
9
+ *
10
+ * カタログ合成・entitlements 判定・取得・sha256 検証・validate-asset・fail-closed の実ロジックは
11
+ * すべて resolver 側の責務のまま変更しない。認証(`~/.akari/store-credentials.json`)も
12
+ * resolver がそのまま読む。ここでは引数をそのまま子プロセスへ転送し、resolver 側の usage /
13
+ * エラー文言 / exit code をそのまま利用者に返すだけ。
14
+ *
15
+ * 規約は他の launcher 委譲コマンド(`akari sounds` = sounds-setup.mjs)と同じ:
16
+ * - 同梱の実体解決は `resolveLauncherAssets()`(モノレポ checkout → npm 配布時の vendor/)
17
+ * - 副作用(spawn)は options で注入可能・node --test で実プロセス不要
18
+ */
19
+
20
+ function defaultSpawnAssetsCli(cliPath, args, env) {
21
+ // stdio: inherit — list の一覧表示・fetch の進捗ログ・JSON 出力をそのまま利用者に見せる
22
+ // (sounds-setup.mjs の defaultSpawnFetch と同じ流儀)。env を明示的に渡すのは必須 —
23
+ // 注入された AKARI_HOME(テスト・隔離実行)が resolver 側の解決にも効くようにする。
24
+ return spawnSync(process.execPath, [cliPath, ...args], { stdio: 'inherit', env });
25
+ }
26
+
27
+ export async function runAssetsCommand(args, options = {}) {
28
+ const logError = options.logError ?? ((line) => console.error(line));
29
+ const env = options.env ?? process.env;
30
+ const assets = options.assets ?? resolveLauncherAssets();
31
+
32
+ const cliPath = assets?.assetResolverCliPath;
33
+ if (!cliPath) {
34
+ logError(assetsResolverUnavailableError());
35
+ return { exitCode: 1 };
36
+ }
37
+
38
+ const spawnAssetsCli = options.spawnAssetsCli ?? defaultSpawnAssetsCli;
39
+ const result = spawnAssetsCli(cliPath, args, env);
40
+ const exitCode = typeof result.status === 'number' ? result.status : (result.error ? 1 : 0);
41
+ return { exitCode };
42
+ }
@@ -0,0 +1,52 @@
1
+ import { spawnSync } from 'node:child_process';
2
+
3
+ import { resolveLauncherAssets } from './repo-assets.mjs';
4
+
5
+ const commands = [
6
+ 'beat-sync-beatmap',
7
+ 'beat-sync-probe-frame',
8
+ 'beat-sync-render-when-idle'
9
+ ];
10
+
11
+ const usage = [
12
+ '使い方: akari internal <subcommand> [args...]',
13
+ '',
14
+ 'サブコマンド:',
15
+ ...commands.map((command) => ` ${command}`)
16
+ ].join('\n');
17
+
18
+ export async function runInternalCommand(args, options = {}) {
19
+ const log = options.log ?? ((line) => console.log(line));
20
+ const logError = options.logError ?? ((line) => console.error(line));
21
+ const assets = options.assets ?? resolveLauncherAssets();
22
+ const spawn = options.spawn ?? spawnSync;
23
+ const subcommand = args[0];
24
+
25
+ if (!subcommand || subcommand === '--help' || subcommand === '-h') {
26
+ log(usage);
27
+ return { exitCode: 0 };
28
+ }
29
+
30
+ const definitions = {
31
+ 'beat-sync-beatmap': { path: assets.beatmapScript, node: true },
32
+ 'beat-sync-probe-frame': { path: assets.probeFrameScript, node: true },
33
+ 'beat-sync-render-when-idle': { path: assets.renderWhenIdleScript, node: false }
34
+ };
35
+ const definition = definitions[subcommand];
36
+ if (!definition) {
37
+ logError(`不明な internal サブコマンドです: ${subcommand}`);
38
+ log(usage);
39
+ return { exitCode: 1 };
40
+ }
41
+ if (!definition.path) {
42
+ logError(`内部コマンド ${subcommand} の実行スクリプトが見つかりません。AKARI Video の完全な checkout または配布物を確認してください。`);
43
+ return { exitCode: 1 };
44
+ }
45
+
46
+ const forwardedArgs = args.slice(1);
47
+ const result = definition.node
48
+ ? spawn(process.execPath, [definition.path, ...forwardedArgs], { stdio: 'inherit' })
49
+ : spawn(definition.path, forwardedArgs, { stdio: 'inherit' });
50
+ const exitCode = typeof result?.status === 'number' ? result.status : 1;
51
+ return { exitCode };
52
+ }
package/src/messages.mjs CHANGED
@@ -48,7 +48,7 @@ export function claudeMissingGuidance() {
48
48
  export function opencodeMissingGuidance() {
49
49
  return [
50
50
  'opencode コマンドが見つかりませんでした。',
51
- 'opencode をインストールしてください: npm install -g opencode',
51
+ 'opencode をインストールしてください: npm install -g opencode-ai',
52
52
  'インストール後、このフォルダーで再度 `akari --opencode` を実行してください。'
53
53
  ].join('\n');
54
54
  }
@@ -191,3 +191,8 @@ export function soundsFailedNotice() {
191
191
  export function soundsUnavailableError() {
192
192
  return '音源セットアップスクリプト(audio-library-setup)が見つかりませんでした。';
193
193
  }
194
+
195
+ /** `akari assets`: 素材 resolver(asset-resolver)が同梱されていない場合のエラー(stderr 1 行)。 */
196
+ export function assetsResolverUnavailableError() {
197
+ return '素材 resolver(asset-resolver)が見つかりませんでした。`akari assets` は使えません。';
198
+ }
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env node
2
-
3
1
  // 原稿テキストから VOICEVOX(ローカル)または fal Qwen3-TTS(自声クローン)でナレーション音声を
4
2
  // 生成し、docs/contract-2026-07-20-edit-json-v1-narration.md 準拠のエントリを組み立てる。
5
3
 
@@ -17,12 +15,20 @@ const FAL_USD_PER_1000_CHARS = 0.09;
17
15
 
18
16
  const usage = [
19
17
  "使い方:",
20
- " node generate-narration.mjs generate \\",
18
+ " akari narration generate \\",
21
19
  " --project <projectDir> --engine <voicevox|fal-qwen3> \\",
22
20
  " --reading-file <読み原稿.txt> [--script-file <表示原稿.txt>] \\",
23
21
  " --t <タイムライン秒> [--gain-db 0] [--id n-0001] \\",
24
22
  " [--speaker 3] [--profile owner-ja] [--dry-run] [--yes] [--apply]",
25
23
  ].join("\n");
24
+ const commandUsage = [
25
+ "使い方: akari narration <subcommand> [options]",
26
+ "",
27
+ "サブコマンド:",
28
+ " generate 原稿からナレーション音声を生成する",
29
+ "",
30
+ usage,
31
+ ].join("\n");
26
32
 
27
33
  class PublicError extends Error {
28
34
  constructor(message, exitCode = 1) {
@@ -31,8 +37,8 @@ class PublicError extends Error {
31
37
  }
32
38
  }
33
39
 
34
- function printJson(value) {
35
- process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
40
+ function printJson(value, log = (line) => console.log(line)) {
41
+ log(JSON.stringify(value, null, 2));
36
42
  }
37
43
 
38
44
  function sleep(ms) {
@@ -390,10 +396,10 @@ async function synthesizeVoicevox(readingText, speakerId) {
390
396
  // --- edit.json への --apply ---
391
397
 
392
398
  function validateEditScriptPath() {
393
- return fileURLToPath(new URL("../../../packages/schemas/bin/validate-edit.mjs", import.meta.url));
399
+ return fileURLToPath(new URL("../../schemas/bin/validate-edit.mjs", import.meta.url));
394
400
  }
395
401
 
396
- function applyToEditJson(projectDir, entry) {
402
+ function applyToEditJson(projectDir, entry, io) {
397
403
  const editPath = path.join(projectDir, "edit.json");
398
404
  let originalText;
399
405
  try {
@@ -422,7 +428,7 @@ function applyToEditJson(projectDir, entry) {
422
428
  `validate-edit が NG のため edit.json への書き込みをロールバックしました。\n${detail}`,
423
429
  );
424
430
  }
425
- process.stderr.write(`edit.json に ${entry.id} を追加しました(validate-edit: PASS)。\n`);
431
+ io.logError(`edit.json に ${entry.id} を追加しました(validate-edit: PASS)。`);
426
432
  }
427
433
 
428
434
  // --- dry-run ---
@@ -432,7 +438,7 @@ function maskKey(secret) {
432
438
  return secret ? "***configured***" : "***unconfigured***";
433
439
  }
434
440
 
435
- async function runDryRun(options, readingText) {
441
+ async function runDryRun(options, readingText, io) {
436
442
  const outputPath = relativeOutputPath(options.id ?? computeNextId(options.project), options.engine);
437
443
  if (options.engine === "voicevox") {
438
444
  printJson({
@@ -449,7 +455,7 @@ async function runDryRun(options, readingText) {
449
455
  speaker: Number(options.speaker),
450
456
  text: readingText,
451
457
  },
452
- });
458
+ }, io.log);
453
459
  return;
454
460
  }
455
461
 
@@ -467,18 +473,18 @@ async function runDryRun(options, readingText) {
467
473
  headers: { Authorization: `Key ${maskKey(falKey)}` },
468
474
  body: payload,
469
475
  },
470
- });
476
+ }, io.log);
471
477
  }
472
478
 
473
479
  // --- generate 本体 ---
474
480
 
475
- async function runGenerate(options) {
481
+ async function runGenerate(options, io) {
476
482
  const readingText = readTextFile(options.readingFile, "読み原稿");
477
483
  const scriptText = options.scriptFile ? readTextFile(options.scriptFile, "表示原稿") : null;
478
484
 
479
485
  if (options.dryRun) {
480
- await runDryRun(options, readingText);
481
- return;
486
+ await runDryRun(options, readingText, io);
487
+ return 0;
482
488
  }
483
489
 
484
490
  const id = options.id ?? computeNextId(options.project);
@@ -514,18 +520,15 @@ async function runGenerate(options) {
514
520
  const falKey = resolveFalKey();
515
521
  const meta = readProfileMeta(options.profile);
516
522
  const estimatedCostUsd = estimateFalCostUsd(readingText);
517
- process.stderr.write(
518
- `fal-qwen3 推定費用: 約 $${estimatedCostUsd}(${readingText.length} 文字 × $${FAL_USD_PER_1000_CHARS}/1000字)\n`,
519
- );
523
+ io.logError(`fal-qwen3 推定費用: 約 $${estimatedCostUsd}(${readingText.length} 文字 × $${FAL_USD_PER_1000_CHARS}/1000字)`);
520
524
  if (!options.yes) {
521
525
  printJson({
522
526
  sent: false,
523
527
  engine: "fal-qwen3",
524
528
  estimated_cost_usd: estimatedCostUsd,
525
529
  reason: "費用承認(--yes)がありません。実リクエストは送信していません。",
526
- });
527
- process.exitCode = 2;
528
- return;
530
+ }, io.log);
531
+ return 2;
529
532
  }
530
533
  // --yes が明示された場合のみ、ここで初めて課金の発生する fal API を呼び出す。
531
534
  audioBuffer = await synthesizeFal(readingText, meta, falKey);
@@ -550,23 +553,37 @@ async function runGenerate(options) {
550
553
  provenance,
551
554
  };
552
555
 
553
- if (options.apply) applyToEditJson(options.project, entry);
556
+ if (options.apply) applyToEditJson(options.project, entry, io);
554
557
 
555
- printJson(entry);
558
+ printJson(entry, io.log);
559
+ return 0;
556
560
  }
557
561
 
558
- async function main() {
559
- let options;
562
+ export async function runNarrationCommand(args, commandOptions = {}) {
563
+ const io = {
564
+ log: commandOptions.log ?? ((line) => console.log(line)),
565
+ logError: commandOptions.logError ?? ((line) => console.error(line)),
566
+ };
567
+
568
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
569
+ io.log(commandUsage);
570
+ return { exitCode: 0 };
571
+ }
572
+ if (args[0] === 'generate' && (args.includes('--help') || args.includes('-h'))) {
573
+ io.log(usage);
574
+ return { exitCode: 0 };
575
+ }
576
+
577
+ let parsedOptions;
560
578
  try {
561
- options = parseArguments(process.argv.slice(2));
562
- await runGenerate(options);
579
+ parsedOptions = parseArguments(args);
580
+ const exitCode = await runGenerate(parsedOptions, io);
581
+ return { exitCode };
563
582
  } catch (error) {
564
583
  const exitCode = error instanceof PublicError ? error.exitCode : 1;
565
584
  const message = error instanceof PublicError ? error.message : `内部処理に失敗しました: ${error?.message ?? error}`;
566
- process.stderr.write(`${message}\n`);
567
- printJson({ error: message });
568
- process.exitCode = exitCode;
585
+ io.logError(message);
586
+ printJson({ error: message }, io.log);
587
+ return { exitCode };
569
588
  }
570
589
  }
571
-
572
- await main();
@@ -0,0 +1,119 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+
5
+ import { resolveLauncherAssets } from './repo-assets.mjs';
6
+
7
+ const usage = '使い方: akari new <target-dir> [--template <path>]';
8
+
9
+ function parseArguments(args) {
10
+ let target;
11
+ let template;
12
+
13
+ for (let index = 0; index < args.length; index += 1) {
14
+ const argument = args[index];
15
+ if (argument === '--template') {
16
+ const value = args[index + 1];
17
+ if (!value || value.startsWith('--')) {
18
+ throw new Error(`${usage}\n--template にはパスが必要です。`);
19
+ }
20
+ template = value;
21
+ index += 1;
22
+ } else if (argument.startsWith('-')) {
23
+ throw new Error(`${usage}\n不明なオプションです: ${argument}`);
24
+ } else if (target === undefined) {
25
+ target = argument;
26
+ } else {
27
+ throw new Error(`${usage}\n作成先は 1 つだけ指定してください。`);
28
+ }
29
+ }
30
+
31
+ if (target === undefined) {
32
+ throw new Error(usage);
33
+ }
34
+ return { target, template };
35
+ }
36
+
37
+ async function directoryHasFile(directory, relativeFile) {
38
+ if (!directory) return false;
39
+ try {
40
+ return (await fs.stat(path.join(directory, relativeFile))).isFile();
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ export async function runNewCommand(args, options = {}) {
47
+ const log = options.log ?? ((line) => console.log(line));
48
+ const logError = options.logError ?? ((line) => console.error(line));
49
+ const cwd = options.cwd ?? process.cwd();
50
+ const assets = options.assets ?? resolveLauncherAssets();
51
+
52
+ if (args.includes('--help') || args.includes('-h')) {
53
+ log(usage);
54
+ return { exitCode: 0 };
55
+ }
56
+
57
+ let parsed;
58
+ try {
59
+ parsed = parseArguments(args);
60
+ } catch (error) {
61
+ logError(error instanceof Error ? error.message : String(error));
62
+ return { exitCode: 1 };
63
+ }
64
+
65
+ const { target, template } = parsed;
66
+ const targetDir = path.resolve(cwd, target);
67
+ const templateDir = template ? path.resolve(cwd, template) : assets.templateDir;
68
+
69
+ if (!templateDir) {
70
+ logError('プロジェクト雛形が見つかりません。');
71
+ return { exitCode: 1 };
72
+ }
73
+
74
+ try {
75
+ const templateStat = await fs.stat(templateDir);
76
+ if (!templateStat.isDirectory()) {
77
+ throw new Error('not a directory');
78
+ }
79
+ } catch {
80
+ logError(`雛形が見つかりません: ${templateDir}`);
81
+ return { exitCode: 1 };
82
+ }
83
+
84
+ const skillsSourceDir = assets.skillsSourceDir;
85
+ const hasSkills = await directoryHasFile(skillsSourceDir, 'analyze-footage/SKILL.md');
86
+ if (!hasSkills) {
87
+ logError(`スキル正本が見つからないため、スキル同梱をスキップします: ${skillsSourceDir ?? '未検出'}`);
88
+ }
89
+ const schemasSourceDir = assets.schemasSourceDir;
90
+ const hasSchemas = await directoryHasFile(schemasSourceDir, 'analysis.schema.json');
91
+
92
+ if (!assets.scaffoldModulePath && !options.createProject) {
93
+ logError('プロジェクト作成モジュールが見つかりません。');
94
+ return { exitCode: 1 };
95
+ }
96
+
97
+ try {
98
+ const createProject = options.createProject ?? await defaultCreateProject(assets.scaffoldModulePath);
99
+ const result = await createProject(targetDir, templateDir, hasSkills
100
+ ? { skillsSourceDir, schemasSourceDir: hasSchemas ? schemasSourceDir : undefined }
101
+ : {});
102
+ log(`プロジェクトを作成しました: ${result.destination}`);
103
+ log(`コピー: ${result.copy.copiedFiles.length} 件`);
104
+ log(`フォールバック補完: ${result.fallback.writtenFiles.length} 件`);
105
+ log(`シンボリックリンクのスキップ: ${result.copy.skippedSymlinks.length} 件`);
106
+ log(`スキル同梱: ${hasSkills ? `実施(${skillsSourceDir})` : 'スキップ'}`);
107
+ log(`git: ${result.git.action}`);
108
+ log(`作成結果レポート: ${result.reportPath}`);
109
+ return { exitCode: 0, result };
110
+ } catch (error) {
111
+ logError(error instanceof Error ? error.message : String(error));
112
+ return { exitCode: 1 };
113
+ }
114
+ }
115
+
116
+ async function defaultCreateProject(scaffoldModulePath) {
117
+ const { createProject } = await import(pathToFileURL(scaffoldModulePath).href);
118
+ return createProject;
119
+ }
@@ -5,13 +5,22 @@ import path from 'node:path';
5
5
  * カレントプロジェクトが AKARI Video プロジェクトとしてセットアップ済みかを判定する。
6
6
  * `.akari/connections.json` の有無を「セットアップ済み」の判定基準にする
7
7
  * (`create-project` / `project-scaffold` が必ずこのファイルを雛形から生成するため)。
8
+ *
9
+ * 例外: `.akari/root.json`(作業場マニフェスト)を持つディレクトリは作業場ルートであって
10
+ * プロジェクトではない。作業場層 `.akari/connections.json`(`packages/creator-root` の
11
+ * `createCreatorRoot()` が既定レジストリを書き出す)が存在しても、それはプロジェクトの
12
+ * セットアップ済みマーカーとして扱わない(さもないと作業場ルート自体が既存プロジェクトと
13
+ * 誤判定され、`(b) 作業場の中だがプロジェクトではない cwd` の新規プロジェクト作成分岐
14
+ * [`first-run.mjs`] が壊れる)。
8
15
  */
9
16
  export function detectProjectState(projectRoot) {
10
17
  const akariDir = path.join(projectRoot, '.akari');
11
18
  const connectionsPath = path.join(akariDir, 'connections.json');
12
19
  const intakePath = path.join(akariDir, 'intake.json');
20
+ const rootManifestPath = path.join(akariDir, 'root.json');
13
21
 
14
- const scaffolded = existsSync(connectionsPath);
22
+ const isWorkspaceRoot = existsSync(rootManifestPath);
23
+ const scaffolded = !isWorkspaceRoot && existsSync(connectionsPath);
15
24
  let intake = null;
16
25
  if (existsSync(intakePath)) {
17
26
  try {
@@ -3,8 +3,8 @@ import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
 
5
5
  // このファイルの場所(packages/akari-launcher/src/)から見て、モノレポの checkout
6
- // なら 2 つ上がリポジトリルート。`skills/create-project/bin/create-project.mjs` と
7
- // 同じ「スクリプト自身の位置からの相対解決」方式(cwd には依存しない — cwd は
6
+ // なら 2 つ上がリポジトリルート。「スクリプト自身の位置からの相対解決」方式で、
7
+ // cwd には依存しない(cwd は
8
8
  // スキャフォールド先のプロジェクトルートであり、リポ checkout の位置とは無関係)。
9
9
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
10
  const DEFAULT_REPO_ROOT_CANDIDATE = path.resolve(PACKAGE_ROOT, '..', '..');
@@ -27,6 +27,13 @@ const CREATOR_ROOT_MODULE_RELATIVE = path.join('packages', 'creator-root', 'src'
27
27
  // 公式音源ライブラリ(AKARI Sounds)の一括取得スクリプト。初回動線(sounds-setup.mjs)と
28
28
  // `akari sounds` が子プロセスとして起動する。未同梱なら null(機能スキップ)。
29
29
  const AUDIO_FETCH_SCRIPT_RELATIVE = path.join('packages', 'audio-library-setup', 'bin', 'fetch-akari-sounds.mjs');
30
+ // 素材 resolver(アカウントの素材 = 無料 + 購入済みの一覧・取得)の CLI 実体。
31
+ // `akari assets <list|fetch|sync|...>`(assets-command.mjs)が子プロセスとして起動する。
32
+ // 未同梱なら null(`akari assets` はその旨のエラーを返す。他コマンドは無影響)。
33
+ const ASSET_RESOLVER_CLI_RELATIVE = path.join('packages', 'asset-resolver', 'bin', 'akari-assets.mjs');
34
+ const BEATMAP_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'beatmap.mjs');
35
+ const PROBE_FRAME_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'probe-frame.mjs');
36
+ const RENDER_WHEN_IDLE_SCRIPT_RELATIVE = path.join('packages', 'akari-tools', 'bin', 'render-when-idle.sh');
30
37
 
31
38
  /**
32
39
  * 指定ルート配下に同梱されているスキル正本・雛形・schemas・scaffold 実装・creator-root
@@ -40,6 +47,10 @@ export function resolveRepoAssets(repoRoot = DEFAULT_REPO_ROOT_CANDIDATE) {
40
47
  const scaffoldModulePath = path.join(repoRoot, SCAFFOLD_MODULE_RELATIVE);
41
48
  const creatorRootModulePath = path.join(repoRoot, CREATOR_ROOT_MODULE_RELATIVE);
42
49
  const audioFetchScriptPath = path.join(repoRoot, AUDIO_FETCH_SCRIPT_RELATIVE);
50
+ const assetResolverCliPath = path.join(repoRoot, ASSET_RESOLVER_CLI_RELATIVE);
51
+ const beatmapScript = path.join(repoRoot, BEATMAP_SCRIPT_RELATIVE);
52
+ const probeFrameScript = path.join(repoRoot, PROBE_FRAME_SCRIPT_RELATIVE);
53
+ const renderWhenIdleScript = path.join(repoRoot, RENDER_WHEN_IDLE_SCRIPT_RELATIVE);
43
54
 
44
55
  return {
45
56
  repoRoot,
@@ -49,7 +60,11 @@ export function resolveRepoAssets(repoRoot = DEFAULT_REPO_ROOT_CANDIDATE) {
49
60
  doctorScript: existsSync(doctorScript) ? doctorScript : null,
50
61
  scaffoldModulePath: existsSync(scaffoldModulePath) ? scaffoldModulePath : null,
51
62
  creatorRootModulePath: existsSync(creatorRootModulePath) ? creatorRootModulePath : null,
52
- audioFetchScriptPath: existsSync(audioFetchScriptPath) ? audioFetchScriptPath : null
63
+ audioFetchScriptPath: existsSync(audioFetchScriptPath) ? audioFetchScriptPath : null,
64
+ assetResolverCliPath: existsSync(assetResolverCliPath) ? assetResolverCliPath : null,
65
+ beatmapScript: existsSync(beatmapScript) ? beatmapScript : null,
66
+ probeFrameScript: existsSync(probeFrameScript) ? probeFrameScript : null,
67
+ renderWhenIdleScript: existsSync(renderWhenIdleScript) ? renderWhenIdleScript : null
53
68
  };
54
69
  }
55
70
 
@@ -64,6 +79,7 @@ export function resolveLauncherAssets({
64
79
  } = {}) {
65
80
  const checkout = resolveRepoAssets(candidateRoot);
66
81
  const found = checkout.skillsSourceDir || checkout.templateDir || checkout.schemasSourceDir
67
- || checkout.doctorScript || checkout.scaffoldModulePath || checkout.creatorRootModulePath;
82
+ || checkout.doctorScript || checkout.scaffoldModulePath || checkout.creatorRootModulePath
83
+ || checkout.beatmapScript || checkout.probeFrameScript || checkout.renderWhenIdleScript;
68
84
  return found ? checkout : resolveRepoAssets(vendorRoot);
69
85
  }
@@ -1,4 +1,13 @@
1
- import { resolveProjectStatus, resolveFullProjectStatus, serializeStatus, formatStatusSummary } from "./status-core/status.mjs";
1
+ import {
2
+ resolveProjectStatus,
3
+ resolveFullProjectStatus,
4
+ serializeStatus,
5
+ formatStatusSummary,
6
+ detectStatusScope,
7
+ resolveWorkspaceStatus,
8
+ serializeWorkspaceStatus,
9
+ formatWorkspaceStatusSummary,
10
+ } from "./status-core/status.mjs";
2
11
 
3
12
  export async function runStatusCommand(argv, options = {}) {
4
13
  const log = options.log ?? ((line) => process.stdout.write(line));
@@ -10,6 +19,14 @@ export async function runStatusCommand(argv, options = {}) {
10
19
  error(cause instanceof Error ? cause.message : String(cause));
11
20
  return { exitCode: 2 };
12
21
  }
22
+ if (detectStatusScope(parsed.projectRoot) === "workspace") {
23
+ const workspace = resolveWorkspaceStatus(parsed.projectRoot);
24
+ // fail-safe: a broken root.json falls through to the unchanged project-scope path below.
25
+ if (workspace) {
26
+ log(parsed.json ? serializeWorkspaceStatus(workspace) : `${formatWorkspaceStatusSummary(workspace)}\n`);
27
+ return { exitCode: 0, workspace };
28
+ }
29
+ }
13
30
  const status = parsed.full
14
31
  ? await resolveFullProjectStatus(parsed.projectRoot)
15
32
  : resolveProjectStatus(parsed.projectRoot, { mode: "fast" });
@@ -0,0 +1,22 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ // プロジェクトの人間向け表示名の解決(task 2026-08-09-project-display-title)。
5
+ // フォルダ名は作成時刻ベースの機械 ID のまま変えない(オーナー裁定: フォルダ名 = 機械の ID /
6
+ // 表示名 = 別に持つ)。人が読む名前は `.akari/intake.json` の `title` に置き、
7
+ // `akari status`(プロジェクトスコープ・作業場スコープ双方)はここを通す。
8
+
9
+ /** `.akari/intake.json` の title を読む。無い・壊れている・型違いは null(フェイルセーフ側)。 */
10
+ export function readProjectTitle(projectRoot) {
11
+ try {
12
+ const parsed = JSON.parse(readFileSync(join(projectRoot, ".akari", "intake.json"), "utf8"));
13
+ return typeof parsed?.title === "string" && parsed.title.trim().length > 0 ? parsed.title : null;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ /** title ?? フォルダ名。title が無い(既存プロジェクト)・null・空文字はフォルダ名へフォールバックする。 */
20
+ export function resolveProjectDisplayName(title, folderName) {
21
+ return typeof title === "string" && title.trim().length > 0 ? title : folderName;
22
+ }
@@ -3,9 +3,17 @@ import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "
3
3
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
4
4
 
5
5
  import { readProjectEvents, resolveActiveAcceptance } from "./events.mjs";
6
+ import { resolveProjectDisplayName } from "./display-name.mjs";
6
7
  import { inspectFullIntegrity } from "./integrity.mjs";
7
8
  import { validateAndCountReview } from "./review.mjs";
8
9
 
10
+ export {
11
+ detectStatusScope,
12
+ resolveWorkspaceStatus,
13
+ serializeWorkspaceStatus,
14
+ formatWorkspaceStatusSummary,
15
+ } from "./workspace.mjs";
16
+
9
17
  const MEDIA_EXTENSIONS = new Set([
10
18
  ".aac", ".aif", ".aiff", ".avi", ".bmp", ".flac", ".gif", ".jpeg", ".jpg",
11
19
  ".m4a", ".m4v", ".mkv", ".mov", ".mp3", ".mp4", ".ogg", ".png", ".wav",
@@ -105,7 +113,7 @@ export function resolveProjectStatus(input = process.cwd(), { mode = "fast" } =
105
113
  return {
106
114
  version: 1,
107
115
  mode,
108
- project: { name: projectName, scaffolded },
116
+ project: { name: projectName, scaffolded, display_name: resolveProjectDisplayName(intake?.title, projectName) },
109
117
  workflow_stage: stage,
110
118
  state_health: stateHealth,
111
119
  waiting_on: routing.waiting_on,