akari-video 1.4.1 → 1.5.0-beta.2

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 (158) hide show
  1. package/package.json +1 -1
  2. package/src/cli.mjs +2 -0
  3. package/src/connect/connectors.json +80 -0
  4. package/src/connect-command.mjs +211 -0
  5. package/src/new-command.mjs +64 -5
  6. package/vendor/.akari-capability-sources.json +3 -0
  7. package/vendor/catalog/audio/camera-shutter/meta.json +7 -0
  8. package/vendor/catalog/audio/candidates-legacy.json +1 -1
  9. package/vendor/catalog/audio/corporate-upbeat-bgm/meta.json +7 -0
  10. package/vendor/catalog/audio/cozy-lofi-bgm/meta.json +7 -0
  11. package/vendor/catalog/audio/dova-syndrome-cheerleaders-bgm/meta.json +7 -0
  12. package/vendor/catalog/audio/dova-syndrome-hatena-mark-se/meta.json +7 -0
  13. package/vendor/catalog/audio/dova-syndrome-user-manual/meta.json +7 -0
  14. package/vendor/catalog/audio/freesound-inspectorj-pencil-writing/meta.json +7 -0
  15. package/vendor/catalog/audio/impact-sfx-pack/meta.json +7 -0
  16. package/vendor/catalog/audio/maoudamashii-bgm-neorock/meta.json +7 -0
  17. package/vendor/catalog/audio/maoudamashii-bgm-piano/meta.json +8 -1
  18. package/vendor/catalog/audio/maoudamashii-object-sound-15/meta.json +7 -0
  19. package/vendor/catalog/audio/maoudamashii-se-magic-category/meta.json +7 -0
  20. package/vendor/catalog/audio/maoudamashii-se-onepoint-category/meta.json +7 -0
  21. package/vendor/catalog/audio/maoudamashii-se-system-category/meta.json +8 -1
  22. package/vendor/catalog/audio/musmus-hageshii-atsui-upper/meta.json +7 -0
  23. package/vendor/catalog/audio/musmus-onomatope-sfx-pack/meta.json +8 -1
  24. package/vendor/catalog/audio/musmus-yuttari-honobono/meta.json +7 -0
  25. package/vendor/catalog/audio/otologic-motion-pop-sfx-pack/meta.json +7 -0
  26. package/vendor/catalog/audio/otologic-sfx-selection/meta.json +8 -1
  27. package/vendor/catalog/audio/pocket-se-fail-pack/meta.json +8 -1
  28. package/vendor/catalog/audio/soundeffect-lab-ambient-life-pack/meta.json +7 -0
  29. package/vendor/catalog/audio/soundeffect-lab-anime-direction-pack/meta.json +7 -0
  30. package/vendor/catalog/audio/soundeffect-lab-scene-transition/meta.json +8 -1
  31. package/vendor/catalog/audio/soundeffect-lab-ui-signal-pack/meta.json +9 -2
  32. package/vendor/catalog/audio/ui-click-sfx-pack/meta.json +7 -0
  33. package/vendor/catalog/audio/whoosh-transition/meta.json +7 -0
  34. package/vendor/catalog/broll/laptop-typing-closeup/meta.json +7 -0
  35. package/vendor/catalog/font/851-chikara-dzuyoku/meta.json +7 -0
  36. package/vendor/catalog/font/851-chikara-yowaku/meta.json +7 -0
  37. package/vendor/catalog/font/ab-kirigirisu/meta.json +10 -0
  38. package/vendor/catalog/font/biz-udgothic/meta.json +10 -0
  39. package/vendor/catalog/font/check-and-oudan/meta.json +7 -0
  40. package/vendor/catalog/font/corporate-logo-rounded/meta.json +10 -0
  41. package/vendor/catalog/font/dela-gothic-one/meta.json +10 -0
  42. package/vendor/catalog/font/dotgothic16/meta.json +10 -0
  43. package/vendor/catalog/font/isego/meta.json +7 -0
  44. package/vendor/catalog/font/klee-one/meta.json +10 -0
  45. package/vendor/catalog/font/kso-kokuryuso/meta.json +7 -0
  46. package/vendor/catalog/font/mgen-plus/meta.json +7 -0
  47. package/vendor/catalog/font/mplus-1p/meta.json +10 -0
  48. package/vendor/catalog/font/mplus-rounded-1c/meta.json +10 -0
  49. package/vendor/catalog/font/noto-sans-jp/meta.json +10 -0
  50. package/vendor/catalog/font/noto-serif-jp/meta.json +10 -0
  51. package/vendor/catalog/font/ranobe-pop/meta.json +7 -0
  52. package/vendor/catalog/font/reggae-one/meta.json +10 -0
  53. package/vendor/catalog/font/rocknroll-one/meta.json +10 -0
  54. package/vendor/catalog/font/ruika/meta.json +10 -0
  55. package/vendor/catalog/font/shippori-mincho/meta.json +10 -0
  56. package/vendor/catalog/font/ta-engeifude/meta.json +10 -0
  57. package/vendor/catalog/font/ta-f1-blockline/meta.json +7 -0
  58. package/vendor/catalog/font/ta-fugafude/meta.json +10 -0
  59. package/vendor/catalog/font/togebara/meta.json +7 -0
  60. package/vendor/catalog/font/vdl-logojr-black/meta.json +7 -0
  61. package/vendor/catalog/font/vdl-v7-mincho/meta.json +7 -0
  62. package/vendor/catalog/font/wanpaku-ruika/meta.json +10 -0
  63. package/vendor/catalog/font/zen-kaku-gothic-new/meta.json +10 -0
  64. package/vendor/catalog/font/zen-maru-gothic/meta.json +10 -0
  65. package/vendor/catalog/font/zero-gothic/meta.json +7 -0
  66. package/vendor/catalog/packs.json +4 -2
  67. package/vendor/catalog/scene3d/modern-smartphone/meta.json +10 -0
  68. package/vendor/catalog/scene3d/studio-hdri/meta.json +10 -0
  69. package/vendor/catalog/scene3d/vintage-camera/meta.json +10 -0
  70. package/vendor/docs/contract-2026-10-15-playbook-v0.md +190 -0
  71. package/vendor/packages/akari-launcher/package.json +1 -1
  72. package/vendor/packages/asset-resolver/src/installed.mjs +4 -0
  73. package/vendor/packages/asset-resolver/src/resolve-async-library.mjs +4 -0
  74. package/vendor/packages/asset-resolver/test/catalog-and-state.test.mjs +11 -0
  75. package/vendor/packages/asset-resolver/test/fragment-project-copy.test.mjs +12 -0
  76. package/vendor/packages/asset-resolver/test/installed-assets.test.mjs +29 -1
  77. package/vendor/packages/asset-resolver/test/resolve-success.test.mjs +22 -0
  78. package/vendor/packages/edit-lint/src/lint/captions.mjs +2 -2
  79. package/vendor/packages/edit-store/lib/caption-display.d.ts +13 -1
  80. package/vendor/packages/edit-store/lib/caption-display.js +57 -15
  81. package/vendor/packages/edit-store/lib/caption-emphasis-match.d.ts +13 -0
  82. package/vendor/packages/edit-store/lib/caption-emphasis-match.js +25 -0
  83. package/vendor/packages/edit-store/lib/caption-look-scale.d.ts +9 -0
  84. package/vendor/packages/edit-store/lib/caption-look-scale.js +137 -0
  85. package/vendor/packages/edit-store/lib/caption-store.d.ts +7 -1
  86. package/vendor/packages/edit-store/lib/caption-store.js +221 -21
  87. package/vendor/packages/edit-store/lib/cut-keyframe-references.d.ts +9 -0
  88. package/vendor/packages/edit-store/lib/cut-keyframe-references.js +89 -0
  89. package/vendor/packages/edit-store/lib/cut-ranges.js +7 -3
  90. package/vendor/packages/edit-store/lib/generated/textstyle-catalog.d.ts +8 -1
  91. package/vendor/packages/edit-store/lib/generated/textstyle-catalog.js +252 -0
  92. package/vendor/packages/edit-store/lib/index.d.ts +3 -0
  93. package/vendor/packages/edit-store/lib/index.js +3 -0
  94. package/vendor/packages/edit-store/lib/retime.js +3 -0
  95. package/vendor/packages/edit-store/lib/ripple.js +5 -1
  96. package/vendor/packages/edit-store/lib/webview-kernel.js +301 -0
  97. package/vendor/packages/i18n/index.cjs +52 -1
  98. package/vendor/packages/i18n/index.d.ts +8 -0
  99. package/vendor/packages/i18n/index.mjs +1 -1
  100. package/vendor/packages/i18n/locales/en/annotations.json +57 -40
  101. package/vendor/packages/i18n/locales/en/annotationscommon.json +32 -5
  102. package/vendor/packages/i18n/locales/en/chat.json +290 -0
  103. package/vendor/packages/i18n/locales/en/cli.json +29 -1
  104. package/vendor/packages/i18n/locales/en/connect.json +57 -0
  105. package/vendor/packages/i18n/locales/en/inspector.json +63 -1
  106. package/vendor/packages/i18n/locales/en/lint-messages.json +2 -1
  107. package/vendor/packages/i18n/locales/en/partner.json +5 -1
  108. package/vendor/packages/i18n/locales/en/preview.json +82 -3
  109. package/vendor/packages/i18n/locales/en/shell.json +3 -1
  110. package/vendor/packages/i18n/locales/en/skills.json +2 -2
  111. package/vendor/packages/i18n/locales/en/surfaces.json +99 -2
  112. package/vendor/packages/i18n/locales/en/transcript.json +33 -1
  113. package/vendor/packages/i18n/locales/index.cjs +4 -0
  114. package/vendor/packages/i18n/locales/ja/annotations.json +54 -37
  115. package/vendor/packages/i18n/locales/ja/annotationscommon.json +32 -5
  116. package/vendor/packages/i18n/locales/ja/chat.json +290 -0
  117. package/vendor/packages/i18n/locales/ja/cli.json +29 -1
  118. package/vendor/packages/i18n/locales/ja/connect.json +57 -0
  119. package/vendor/packages/i18n/locales/ja/inspector.json +63 -1
  120. package/vendor/packages/i18n/locales/ja/lint-messages.json +2 -1
  121. package/vendor/packages/i18n/locales/ja/lint.json +1 -1
  122. package/vendor/packages/i18n/locales/ja/partner.json +5 -1
  123. package/vendor/packages/i18n/locales/ja/preview.json +82 -3
  124. package/vendor/packages/i18n/locales/ja/shell.json +3 -1
  125. package/vendor/packages/i18n/locales/ja/skills.json +2 -2
  126. package/vendor/packages/i18n/locales/ja/surfaces.json +99 -2
  127. package/vendor/packages/i18n/locales/ja/transcript.json +33 -1
  128. package/vendor/packages/i18n/test/annotation-copy.test.mjs +15 -0
  129. package/vendor/packages/i18n/test/asset-text.test.mjs +90 -0
  130. package/vendor/packages/media-bin/src/preview-audio-sidecar.mjs +1 -1
  131. package/vendor/packages/media-bin/test/preview-audio-sidecar-status.test.mjs +15 -0
  132. package/vendor/packages/project-scaffold/src/index.mjs +173 -12
  133. package/vendor/packages/schemas/asset-meta.schema.json +16 -0
  134. package/vendor/packages/schemas/bin/validate-asset.mjs +35 -0
  135. package/vendor/packages/schemas/bin/validate-tasks.mjs +0 -0
  136. package/vendor/packages/schemas/playbook.schema.json +97 -0
  137. package/vendor/packages/schemas/test/asset-i18n.test.mjs +47 -0
  138. package/vendor/packages/schemas/test/fixtures/asset-i18n/invalid-field.json +90 -0
  139. package/vendor/packages/schemas/test/fixtures/asset-i18n/invalid-ja.json +90 -0
  140. package/vendor/packages/schemas/test/fixtures/asset-i18n/invalid-knob.json +92 -0
  141. package/vendor/packages/schemas/test/fixtures/asset-i18n/invalid-type.json +90 -0
  142. package/vendor/packages/schemas/test/fixtures/asset-i18n/valid.json +98 -0
  143. package/vendor/packages/schemas/test/validate-asset.test.mjs +24 -0
  144. package/vendor/packages/schemas/test/validate-playbook.test.mjs +34 -0
  145. package/vendor/packages/schemas/validate-playbook.mjs +56 -0
  146. package/vendor/presets/luts/bake-previews.mjs +8 -2
  147. package/vendor/presets/luts/index.jsonl +10 -10
  148. package/vendor/presets/luts/previews.test.mjs +4 -0
  149. package/vendor/presets/textanim/index.jsonl +47 -47
  150. package/vendor/skills/compile-review-session/SKILL.md +1 -1
  151. package/vendor/skills/connect/SKILL.md +24 -0
  152. package/vendor/skills/create-project/SKILL.md +4 -0
  153. package/vendor/skills/manage-connections/SKILL.md +4 -0
  154. package/vendor/skills/manage-connections/bin/doctor.mjs +44 -3
  155. package/vendor/skills/manage-connections/test/connectors-doctor.test.mjs +46 -0
  156. package/vendor/skills/record-playbook/SKILL.md +24 -0
  157. package/vendor/skills/record-playbook/bin/record-playbook.mjs +248 -0
  158. package/vendor/skills/research-plan/lang/ja.md +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "1.4.1",
3
+ "version": "1.5.0-beta.2",
4
4
  "description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -29,6 +29,7 @@ import { runMediaCommand } from './media-command.mjs';
29
29
  import { runSpeakersCommand } from './speakers-command.mjs';
30
30
  import { runToolsCommand } from './tools-command.mjs';
31
31
  import { runWordBookCommand } from './word-book-command.mjs';
32
+ import { runConnectCommand } from './connect-command.mjs';
32
33
  import { resolveRuntimePaths } from './runtime-diagnostics.mjs';
33
34
  import { cliText } from './i18n.mjs';
34
35
 
@@ -41,6 +42,7 @@ export const runSubcommands = {
41
42
  speakers: runSpeakersCommand,
42
43
  tools: runToolsCommand,
43
44
  'word-book': runWordBookCommand,
45
+ connect: runConnectCommand,
44
46
  };
45
47
 
46
48
  /**
@@ -0,0 +1,80 @@
1
+ [
2
+ {
3
+ "id": "akari-video",
4
+ "group": "tools",
5
+ "name": "AKARI Video",
6
+ "description": "動画を企画・編集する",
7
+ "icon": "A",
8
+ "kind": "built-in",
9
+ "try": "動画を作って"
10
+ },
11
+ {
12
+ "id": "3d-recipes",
13
+ "group": "tools",
14
+ "name": "3D レシピ",
15
+ "description": "Blender で 3D 素材を作る",
16
+ "icon": "3D",
17
+ "kind": "blender-recipe",
18
+ "try": "ロゴを 3D にして"
19
+ },
20
+ {
21
+ "id": "fal",
22
+ "group": "generate",
23
+ "name": "fal",
24
+ "description": "画像・映像・読み上げを作る",
25
+ "icon": "f",
26
+ "kind": "key",
27
+ "env": "FAL_KEY",
28
+ "setupUrl": "https://fal.ai/dashboard/keys",
29
+ "try": "商品を白背景で撮った写真を 4 枚",
30
+ "maker": "fal"
31
+ },
32
+ {
33
+ "id": "fish-audio",
34
+ "group": "generate",
35
+ "name": "Fish Audio",
36
+ "description": "原稿を読み上げる",
37
+ "icon": "F",
38
+ "kind": "key",
39
+ "env": "FISH_AUDIO_API_KEY",
40
+ "setupUrl": "https://fish.audio/app/api-keys/",
41
+ "try": "この原稿を読み上げて",
42
+ "maker": "fish"
43
+ },
44
+ {
45
+ "id": "google-ai",
46
+ "group": "generate",
47
+ "name": "Google AI",
48
+ "description": "原稿の読み上げ・声の写しを作る",
49
+ "icon": "G",
50
+ "kind": "key",
51
+ "env": "GEMINI_API_KEY",
52
+ "setupUrl": "https://aistudio.google.com/apikey",
53
+ "try": "この原稿を読み上げて",
54
+ "maker": "google"
55
+ },
56
+ {
57
+ "id": "elevenlabs",
58
+ "group": "generate",
59
+ "name": "ElevenLabs",
60
+ "description": "音声を文字起こしする",
61
+ "icon": "E",
62
+ "kind": "key",
63
+ "env": "ELEVENLABS_API_KEY",
64
+ "setupUrl": "https://elevenlabs.io/app/settings/api-keys",
65
+ "try": "この音声を文字起こしして",
66
+ "maker": "elevenlabs"
67
+ },
68
+ {
69
+ "id": "groq",
70
+ "group": "generate",
71
+ "name": "Groq",
72
+ "description": "音声を文字起こしする",
73
+ "icon": "G",
74
+ "kind": "key",
75
+ "env": "GROQ_API_KEY",
76
+ "setupUrl": "https://console.groq.com/keys",
77
+ "try": "この音声を文字起こしして",
78
+ "maker": "groq"
79
+ }
80
+ ]
@@ -0,0 +1,211 @@
1
+ import { cliText as t } from './i18n.mjs';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { spawn } from 'node:child_process';
6
+ import { constants as osConstants } from 'node:os';
7
+ import { StringDecoder } from 'node:string_decoder';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const catalog = JSON.parse(fs.readFileSync(fileURLToPath(new URL('./connect/connectors.json', import.meta.url)), 'utf8'));
11
+
12
+ const generation = catalog.filter(row => row.group === 'generate').map(row => row.id);
13
+ const usage = () => t('cli.connectCommand.ui.usageText');
14
+ const groupLabel = group => t(group === 'tools' ? 'cli.connectCommand.group.toolsLabel' : 'cli.connectCommand.group.generateLabel');
15
+ const statusLabel = status => t({
16
+ ready: 'cli.connectCommand.ui.availableLabel', planned: 'cli.connectCommand.ui.comingSoonLabel',
17
+ 'needs-key': 'cli.connectCommand.ui.keyRequiredLabel', 'needs-blender': 'cli.connectCommand.ui.requiresBlenderLabel'
18
+ }[status]);
19
+
20
+ export function connectPaths(env = process.env, home = os.homedir()) {
21
+ return {
22
+ settings: path.join(env.AKARI_HOME || path.join(home, '.akari'), 'connectors.json'),
23
+ primary: env.AKARI_CREDENTIALS_FILE || path.join(env.AKARI_HOME || path.join(home, '.akari'), 'credentials.env'),
24
+ legacy: env.AKARI_CREDENTIALS_FILE ? null : path.join(home, '.config', 'akari-video', 'credentials.env')
25
+ };
26
+ }
27
+ function readSettings(file) {
28
+ try {
29
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
30
+ return {
31
+ enabled: typeof data.enabled === 'object' && data.enabled ? data.enabled : {},
32
+ generationOrder: Array.isArray(data.generationOrder) ? data.generationOrder.filter(id => generation.includes(id)) : [],
33
+ fallbackOnFailure: data.fallbackOnFailure === true
34
+ };
35
+ } catch (error) {
36
+ if (error.code === 'ENOENT') return { enabled: {}, generationOrder: [], fallbackOnFailure: false };
37
+ throw new Error(t('cli.connectCommand.ui.couldNotReadConnectionSettingsError'));
38
+ }
39
+ }
40
+ function readCredentialFile(file) {
41
+ const values = new Map();
42
+ if (!file) return values;
43
+ let content;
44
+ try {
45
+ const stat = fs.lstatSync(file);
46
+ if (!stat.isFile()) throw new Error();
47
+ content = fs.readFileSync(file, 'utf8');
48
+ } catch (error) {
49
+ if (error.code === 'ENOENT') return values;
50
+ throw new Error(t('cli.connectCommand.ui.couldNotReadCredentialsFileError'));
51
+ }
52
+ for (const raw of content.split(/\r?\n/)) {
53
+ const line = raw.trim();
54
+ if (!line || line.startsWith('#')) continue;
55
+ const index = line.indexOf('=');
56
+ const name = line.slice(0, index).trim();
57
+ if (index < 1 || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue;
58
+ let value = line.slice(index + 1).trim();
59
+ if (value.length >= 2 && (/^".*"$/.test(value) || /^'.*'$/.test(value))) value = value.slice(1, -1);
60
+ values.set(name, value);
61
+ }
62
+ return values;
63
+ }
64
+ function credentialValues(paths, env) {
65
+ const values = new Map();
66
+ for (const [name, value] of readCredentialFile(paths.legacy)) if (value.trim()) values.set(name, { value, source: 'credentials.env' });
67
+ for (const [name, value] of readCredentialFile(paths.primary)) if (value.trim()) values.set(name, { value, source: 'credentials.env' });
68
+ for (const entry of catalog) if (entry.env && typeof env[entry.env] === 'string' && env[entry.env].trim()) {
69
+ values.set(entry.env, { value: env[entry.env], source: 'env' });
70
+ }
71
+ return values;
72
+ }
73
+ function dirNames(root) {
74
+ try { return fs.readdirSync(root, { withFileTypes: true }).filter(entry => entry.isDirectory()).map(entry => entry.name); }
75
+ catch { return []; }
76
+ }
77
+ export function detectApp(id, roots, platform = process.platform) {
78
+ if (id !== 'blender') return false;
79
+ const pattern = platform === 'win32' ? /^(?:Blender Foundation|Blender(?: \d+)?)$/i : /^Blender(?: \d+(?:\.\d+)*)?\.app$/i;
80
+ return roots.some(root => dirNames(root).some(name => pattern.test(name)));
81
+ }
82
+ function detectBlenderCommand(env, platform) {
83
+ const executable = platform === 'win32' ? 'blender.exe' : 'blender';
84
+ const candidates = [env.BLENDER, ...(env.PATH || '').split(platform === 'win32' ? ';' : ':').map(directory => path.join(directory, executable))];
85
+ return candidates.some(candidate => {
86
+ if (!candidate) return false;
87
+ try { fs.accessSync(candidate, fs.constants.X_OK); return fs.statSync(candidate).isFile(); } catch { return false; }
88
+ });
89
+ }
90
+ export function listConnectors({ env = process.env, home = os.homedir(), platform = process.platform, roots } = {}) {
91
+ const paths = connectPaths(env, home);
92
+ const settings = readSettings(paths.settings);
93
+ const credentials = credentialValues(paths, env);
94
+ const scanRoots = roots || (platform === 'win32' ? [env.ProgramFiles, env['ProgramFiles(x86)']].filter(Boolean) : ['/Applications', path.join(home, 'Applications')]);
95
+ const order = [...new Set(settings.generationOrder), ...generation.filter(id => !settings.generationOrder.includes(id))];
96
+ const rows = catalog.map(entry => {
97
+ const detected = entry.kind === 'blender-recipe' ? detectApp('blender', scanRoots, platform) || (!roots && detectBlenderCommand(env, platform)) : false;
98
+ const credential = entry.env ? credentials.get(entry.env) : undefined;
99
+ const hasKey = !!credential;
100
+ const status = entry.kind === 'built-in' ? 'ready' : entry.kind === 'planned' ? 'planned' :
101
+ entry.kind === 'key' ? hasKey ? 'ready' : 'needs-key' :
102
+ entry.kind === 'blender-recipe' ? detected ? 'ready' : 'needs-blender' : 'planned';
103
+ return { id: entry.id, group: entry.group, name: entry.name, status, enabled: entry.kind === 'built-in' || settings.enabled[entry.id] !== false,
104
+ detected, credential: entry.env ? hasKey : null, credentialSource: credential?.source ?? null };
105
+ });
106
+ rows.sort((a, b) => a.group === 'generate' && b.group === 'generate' ? order.indexOf(a.id) - order.indexOf(b.id) : 0);
107
+ return { rows, generationOrder: order, fallbackOnFailure: settings.fallbackOnFailure };
108
+ }
109
+
110
+ export function createSecretRedactor(secret, emit) {
111
+ const decoder = new StringDecoder('utf8');
112
+ let pending = '';
113
+ const write = text => { if (text) emit(text); };
114
+ const longestPrefixSuffix = () => {
115
+ for (let length = Math.min(secret.length - 1, pending.length); length > 0; length--) {
116
+ if (secret.startsWith(pending.slice(-length))) return length;
117
+ }
118
+ return 0;
119
+ };
120
+ const drain = final => {
121
+ while (pending) {
122
+ const found = pending.indexOf(secret);
123
+ if (found >= 0) {
124
+ write(pending.slice(0, found));
125
+ write('[REDACTED]');
126
+ pending = pending.slice(found + secret.length);
127
+ continue;
128
+ }
129
+ const held = longestPrefixSuffix();
130
+ write(pending.slice(0, pending.length - held));
131
+ if (final && held) write('[REDACTED]');
132
+ pending = final ? '' : pending.slice(pending.length - held);
133
+ return;
134
+ }
135
+ };
136
+ return {
137
+ write(chunk) { pending += decoder.write(chunk); drain(false); },
138
+ end() { pending += decoder.end(); drain(true); }
139
+ };
140
+ }
141
+
142
+ async function forwardStream(stream, secret, emit) {
143
+ if (!stream) return;
144
+ const redactor = createSecretRedactor(secret, emit);
145
+ for await (const chunk of stream) redactor.write(chunk);
146
+ redactor.end();
147
+ }
148
+
149
+ export async function runConnectCommand(args, options = {}) {
150
+ const log = options.log || (line => process.stdout.write(`${line}\n`));
151
+ const error = options.error || (line => process.stderr.write(`${line}\n`));
152
+ const stdoutWrite = options.stdoutWrite || (chunk => process.stdout.write(chunk));
153
+ const stderrWrite = options.stderrWrite || (chunk => process.stderr.write(chunk));
154
+ const env = options.env || process.env;
155
+ const home = options.home || os.homedir();
156
+ if (args[0] === 'list' && (args.length === 1 || args.length === 2 && args[1] === '--json')) {
157
+ const data = listConnectors({ env, home, roots: options.roots, platform: options.platform });
158
+ if (args[1] === '--json') log(JSON.stringify({ ...data, rows: data.rows.map(row => ({
159
+ ...row, group: groupLabel(row.group), status: statusLabel(row.status)
160
+ })) }));
161
+ else {
162
+ let group = '';
163
+ for (const row of data.rows) {
164
+ if (row.group !== group) { group = row.group; log(groupLabel(group)); }
165
+ log(` ${row.name}: ${statusLabel(row.status)}${row.enabled ? '' : t('cli.connectCommand.ui.disabledLabel')}`);
166
+ }
167
+ }
168
+ return { exitCode: 0 };
169
+ }
170
+ if (args[0] === 'exec' && args.length >= 4 && args[2] === '--') {
171
+ const entry = catalog.find(row => row.id === args[1]);
172
+ if (!entry?.env) { error(t('cli.connectCommand.ui.connectionCannotRunAPIKeyError')); return { exitCode: 2 }; }
173
+ const settings = readSettings(connectPaths(env, home).settings);
174
+ if (settings.enabled[entry.id] === false) { error(t('cli.connectCommand.ui.connectionDisabledMessage')); return { exitCode: 2 }; }
175
+ const credential = credentialValues(connectPaths(env, home), env).get(entry.env);
176
+ if (!credential) { error(t('cli.connectCommand.ui.noKeyHasBeenSetMessage')); return { exitCode: 2 }; }
177
+ const secret = credential.value;
178
+ const childEnv = { ...env };
179
+ for (const connector of catalog) if (connector.env) delete childEnv[connector.env];
180
+ // 旧カタログの鍵も子プロセスへ漏らさない。
181
+ delete childEnv.HF_KEY;
182
+ delete childEnv.RUNWAYML_API_SECRET;
183
+ childEnv[entry.env] = secret;
184
+ let child;
185
+ try {
186
+ child = (options.spawn || spawn)(args[3], args.slice(4), {
187
+ shell: false, env: childEnv, stdio: ['inherit', 'pipe', 'pipe']
188
+ });
189
+ } catch {
190
+ error(t('cli.connectCommand.ui.couldNotStartChildProcessError'));
191
+ return { exitCode: 1 };
192
+ }
193
+ let launchFailed = false;
194
+ const closed = new Promise(resolve => {
195
+ child.once('error', () => { launchFailed = true; });
196
+ child.once('close', (code, signal) => {
197
+ const signalCode = signal ? osConstants.signals[signal] : undefined;
198
+ resolve(typeof code === 'number' ? code : typeof signalCode === 'number' ? 128 + signalCode : 1);
199
+ });
200
+ });
201
+ const [exitCode] = await Promise.all([
202
+ closed,
203
+ forwardStream(child.stdout, secret, stdoutWrite),
204
+ forwardStream(child.stderr, secret, stderrWrite)
205
+ ]);
206
+ if (launchFailed) error(t('cli.connectCommand.ui.couldNotStartChildProcessError'));
207
+ return { exitCode };
208
+ }
209
+ error(usage());
210
+ return { exitCode: 2 };
211
+ }
@@ -1,9 +1,11 @@
1
1
  import fs from 'node:fs/promises';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { pathToFileURL } from 'node:url';
4
5
 
5
6
  import { resolveLauncherAssets } from './repo-assets.mjs';
6
7
  import { cliText } from './i18n.mjs';
8
+ import { findExecutable } from './path-lookup.mjs';
7
9
 
8
10
  const usage = (lang) => cliText('cli.new.help.usage', {}, lang);
9
11
  const languageTagPattern = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
@@ -11,6 +13,7 @@ const languageTagPattern = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
11
13
  function parseArguments(args) {
12
14
  let target;
13
15
  let template;
16
+ let from;
14
17
  let lang;
15
18
 
16
19
  for (let index = 0; index < args.length; index += 1) {
@@ -22,6 +25,13 @@ function parseArguments(args) {
22
25
  }
23
26
  template = value;
24
27
  index += 1;
28
+ } else if (argument === '--from') {
29
+ const value = args[index + 1];
30
+ if (!value || value.startsWith('--')) {
31
+ throw new Error(`${usage()}\n${cliText('cli.new.error.fromMissing', {})}`);
32
+ }
33
+ from = value;
34
+ index += 1;
25
35
  } else if (argument === '--lang') {
26
36
  const value = args[index + 1];
27
37
  if (!value || !languageTagPattern.test(value)) {
@@ -41,7 +51,7 @@ function parseArguments(args) {
41
51
  if (target === undefined) {
42
52
  throw new Error(usage());
43
53
  }
44
- return { target, template, lang };
54
+ return { target, template, from, lang };
45
55
  }
46
56
 
47
57
  async function directoryHasFile(directory, relativeFile) {
@@ -53,6 +63,28 @@ async function directoryHasFile(directory, relativeFile) {
53
63
  }
54
64
  }
55
65
 
66
+ async function resolvePlaybookDir(from, { cwd, targetDir, assets, env = process.env }) {
67
+ const explicit = path.resolve(cwd, from);
68
+ if (await directoryHasFile(explicit, 'playbook.json')) return explicit;
69
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(from)) return null;
70
+ if (assets.creatorRootModulePath) {
71
+ const { resolveCreatorRoot } = await import(pathToFileURL(assets.creatorRootModulePath).href);
72
+ const root = await resolveCreatorRoot({ cwd, env });
73
+ if (root?.error) throw root.error;
74
+ const relative = root && path.relative(root.rootDir, targetDir);
75
+ const segments = relative?.split(path.sep) ?? [];
76
+ const fromTarget = segments[0] === 'channels' ? segments[1] : null;
77
+ const channel = fromTarget && root?.manifest?.channels?.includes(fromTarget)
78
+ ? fromTarget : root?.manifest?.channels?.[0];
79
+ if (root && channel) {
80
+ const candidate = path.join(root.rootDir, 'channels', channel, 'playbooks', from);
81
+ if (await directoryHasFile(candidate, 'playbook.json')) return candidate;
82
+ }
83
+ }
84
+ const local = path.join(env.AKARI_HOME || path.join(os.homedir(), '.akari'), 'playbooks', from);
85
+ return await directoryHasFile(local, 'playbook.json') ? local : null;
86
+ }
87
+
56
88
  export async function runNewCommand(args, options = {}) {
57
89
  const text = (key, params) => cliText(key, params);
58
90
  const log = options.log ?? ((line) => console.log(line));
@@ -62,6 +94,7 @@ export async function runNewCommand(args, options = {}) {
62
94
 
63
95
  if (args.includes('--help') || args.includes('-h')) {
64
96
  log(usage());
97
+ log(text('cli.new.help.fromUsage'));
65
98
  return { exitCode: 0 };
66
99
  }
67
100
 
@@ -73,9 +106,18 @@ export async function runNewCommand(args, options = {}) {
73
106
  return { exitCode: 1 };
74
107
  }
75
108
 
76
- const { target, template, lang } = parsed;
109
+ const { target, template, from, lang } = parsed;
77
110
  const targetDir = path.resolve(cwd, target);
78
111
  const templateDir = template ? path.resolve(cwd, template) : assets.templateDir;
112
+ let playbookDir;
113
+ if (from) {
114
+ try { playbookDir = await resolvePlaybookDir(from, { cwd, targetDir, assets, env: options.env }); }
115
+ catch (error) { logError(error.message); return { exitCode: 1 }; }
116
+ if (!playbookDir) {
117
+ logError(text('cli.new.error.fromNotFound', { from }));
118
+ return { exitCode: 1 };
119
+ }
120
+ }
79
121
 
80
122
  if (!templateDir) {
81
123
  logError(text('cli.new.error.templateUnavailable'));
@@ -107,9 +149,21 @@ export async function runNewCommand(args, options = {}) {
107
149
 
108
150
  try {
109
151
  const createProject = options.createProject ?? await defaultCreateProject(assets.scaffoldModulePath);
110
- const result = await createProject(targetDir, templateDir, hasSkills
152
+ const scaffoldOptions = hasSkills
111
153
  ? { skillsSourceDir, schemasSourceDir: hasSchemas ? schemasSourceDir : undefined }
112
- : {});
154
+ : {};
155
+ let applied;
156
+ if (playbookDir) {
157
+ const scaffold = options.scaffold ?? await import(pathToFileURL(assets.scaffoldModulePath).href);
158
+ const env = options.env ?? process.env;
159
+ scaffoldOptions.beforeCommit = async (destination) => {
160
+ applied = await scaffold.applyPlaybook({ projectDir: destination, playbookDir,
161
+ hasTool: (name) => Boolean(findExecutable(name, env.PATH ?? process.env.PATH ?? '', process.platform,
162
+ env.PATHEXT ?? process.env.PATHEXT)) });
163
+ return applied;
164
+ };
165
+ }
166
+ const result = await createProject(targetDir, templateDir, scaffoldOptions);
113
167
  if (lang) {
114
168
  const intakePath = path.join(targetDir, '.akari', 'intake.json');
115
169
  const intake = JSON.parse(await fs.readFile(intakePath, 'utf8'));
@@ -123,7 +177,12 @@ export async function runNewCommand(args, options = {}) {
123
177
  log(text('cli.new.result.skills', { status: hasSkills ? text('cli.new.label.included', { path: skillsSourceDir }) : text('cli.new.label.skipped') }));
124
178
  log(`git: ${result.git.action}`);
125
179
  log(text('cli.new.result.report', { path: result.reportPath }));
126
- return { exitCode: 0, result };
180
+ if (applied) {
181
+ log(text('cli.new.result.fromApplied', { path: applied.playbookJsonPath }));
182
+ log(text('cli.new.result.sheet', { path: applied.sheetPath }));
183
+ for (const tool of applied.missingTools) log(text('cli.new.warning.toolMissing', { tool }));
184
+ }
185
+ return { exitCode: 0, result, applied };
127
186
  } catch (error) {
128
187
  logError(error instanceof Error ? error.message : String(error));
129
188
  return { exitCode: 1 };
@@ -90,6 +90,7 @@
90
90
  "docs/contract-2026-10-11-speaker-split-v0.md",
91
91
  "docs/contract-2026-10-11-ui-i18n.md",
92
92
  "docs/contract-2026-10-12-caption-look-v1.md",
93
+ "docs/contract-2026-10-15-playbook-v0.md",
93
94
  "packages/akari-ear/package.json",
94
95
  "packages/akari-ear/README.md",
95
96
  "packages/akari-launcher/package.json",
@@ -180,6 +181,7 @@
180
181
  "skills/compile-review-session/dev-fixtures/issue-79/README.md",
181
182
  "skills/compile-review-session/dev-fixtures/README.md",
182
183
  "skills/compile-review-session/SKILL.md",
184
+ "skills/connect/SKILL.md",
183
185
  "skills/create-project/SKILL.md",
184
186
  "skills/critique-cut/SKILL.md",
185
187
  "skills/critique-cut/workflow.md",
@@ -236,6 +238,7 @@
236
238
  "skills/overlay-authoring/text-behind-person.md",
237
239
  "skills/overlay-authoring/thumbnail.md",
238
240
  "skills/overlay-authoring/world.md",
241
+ "skills/record-playbook/SKILL.md",
239
242
  "skills/render-cut/SKILL.md",
240
243
  "skills/research-plan/competitor.md",
241
244
  "skills/research-plan/ideate.md",
@@ -4,6 +4,13 @@
4
4
  "title": "カメラシャッター SFX",
5
5
  "description": "一眼レフ実機(Canon T2i)のシャッター音。写真の切り替えや決定的瞬間の演出に使える短尺 SFX。尺 0.3秒",
6
6
  "when_to_use": "写真ギャラリー/スライドショーのカット送り、Before/After 比較、決定的瞬間を強調する演出",
7
+ "i18n": {
8
+ "en": {
9
+ "title": "Camera Shutter SFX",
10
+ "description": "The shutter of a real Canon T2i DSLR. A short 0.3-second effect for switching photos or marking a decisive moment.",
11
+ "when_to_use": "For advancing photos in galleries or slideshows, before/after comparisons, and emphasizing decisive moments."
12
+ }
13
+ },
7
14
  "tags": ["sfx", "camera", "shutter", "photo", "snap"],
8
15
  "knobs": [],
9
16
  "ai_usage": "音量調整・複数回の重ね掛けは自由。映像のタイミングに合わせたトリムも可。ピッチ・速度を大きく変えると素材の質感が崩れるため軽微な調整に留める",
@@ -1,5 +1,5 @@
1
1
  {
2
- "$comment": "setup-audio-library 候補リストのデータ SSOT。skills/setup-audio-library と packages/audio-library-setup/bin/generate-candidates-html.mjs がこのファイルを読む。根拠: research/2026-07-22-free-sfx-bgm-sources.md(内部リポ)。リンクは必ずダウンロードページ URL(音声ファイル直 URL は禁止 — 効果音ラボ/甘茶/H-MIX GALLERY 等の直リンク禁止規約対応)。",
2
+ "$comment": "setup-audio-library 候補リストのデータ SSOT。skills/setup-audio-library と packages/audio-library-setup/bin/generate-candidates-html.mjs がこのファイルを読む。リンクは必ずダウンロードページ URL(音声ファイル直 URL は禁止 — 効果音ラボ/甘茶/H-MIX GALLERY 等の直リンク禁止規約対応)。",
3
3
  "version": 1,
4
4
  "generated_at": "2026-07-22",
5
5
  "source_research": "internal: research/2026-07-22-free-sfx-bgm-sources.md",
@@ -4,6 +4,13 @@
4
4
  "title": "コーポレート・アップビート BGM",
5
5
  "description": "ミニマルなビート主体の明るいコーポレート BGM。テック企業のプロモーションや製品デモに合う清潔感のあるトラック。尺 2分05秒",
6
6
  "when_to_use": "SaaS/アプリ紹介、製品デモ、会社紹介動画のオープニング〜本編 BGM として",
7
+ "i18n": {
8
+ "en": {
9
+ "title": "Upbeat Corporate BGM",
10
+ "description": "Bright corporate background music built around a minimal beat. A clean track suited to tech-company promotions and product demos. Duration: 2 minutes 05 seconds.",
11
+ "when_to_use": "For opening and main-section music in SaaS or app introductions, product demos, and company-profile videos."
12
+ }
13
+ },
7
14
  "tags": ["bgm", "corporate", "upbeat", "tech", "background-music"],
8
15
  "knobs": [],
9
16
  "ai_usage": "書き出し尺に合わせたトリミング・フェードイン/アウト・ループでの尺調整は自由に行ってよい。ナレーションやセリフが乗る区間は自動でダッキング(音量を下げる)してよい。曲の構成やテンポそのものを崩す大幅なピッチ/速度変更は避ける",
@@ -4,6 +4,13 @@
4
4
  "title": "コージー Lo-fi チル BGM",
5
5
  "description": "Pixabay Editor's Choice 選出のローファイ・チルビート。落ち着いた質感で Vlog や作業風景、夜のシーンに合う。尺 2分27秒",
6
6
  "when_to_use": "Vlog、作業・勉強シーン、就寝前コンテンツなど落ち着いたトーンの BGM が欲しいシーン",
7
+ "i18n": {
8
+ "en": {
9
+ "title": "Cozy Lo-fi Chill BGM",
10
+ "description": "A lo-fi chill beat selected as a Pixabay Editor's Choice. Its mellow texture suits vlogs, work scenes, and nighttime footage. Duration: 2 minutes 27 seconds.",
11
+ "when_to_use": "For vlogs, work or study scenes, and bedtime content needing calm background music."
12
+ }
13
+ },
7
14
  "tags": ["bgm", "lofi", "chill", "cozy", "background-music"],
8
15
  "knobs": [],
9
16
  "ai_usage": "書き出し尺に合わせたトリミング・フェードイン/アウト・ループでの尺調整は自由に行ってよい。ナレーションやセリフが乗る区間は自動でダッキング(音量を下げる)してよい。曲の構成やテンポそのものを崩す大幅なピッチ/速度変更は避ける",
@@ -4,6 +4,13 @@
4
4
  "title": "DOVA-SYNDROME Cheerleaders(BGM)",
5
5
  "description": "明るく楽しい雰囲気のアップテンポBGM。DOVA-SYNDROME公式YouTubeチャンネルでもプレビュー公開されている楽曲。(収録数: 1点)",
6
6
  "when_to_use": "明るい/楽しい雰囲気を出したい場面のBGM、応援・盛り上げ系のシーンに",
7
+ "i18n": {
8
+ "en": {
9
+ "title": "DOVA-SYNDROME Cheerleaders (BGM)",
10
+ "description": "Up-tempo background music with a bright, fun mood. A preview is also published on DOVA-SYNDROME's official YouTube channel. Includes 1 item.",
11
+ "when_to_use": "For cheerful background music, encouragement, and high-energy scenes."
12
+ }
13
+ },
7
14
  "tags": [
8
15
  "bgm",
9
16
  "upbeat",
@@ -4,6 +4,13 @@
4
4
  "title": "DOVA-SYNDROME はてなマーク効果音",
5
5
  "description": "フレクサトーン音を加工した「ぽよ〜ん」とした質問符(?)ポップアップ効果音。キャラクターが困惑・疑問を示す場面向け。(収録数: 1点)",
6
6
  "when_to_use": "キャラクターの頭上に「?」が浮かぶ疑問・困惑の一瞬を表現したいとき",
7
+ "i18n": {
8
+ "en": {
9
+ "title": "DOVA-SYNDROME Question Mark SFX",
10
+ "description": "A springy question-mark popup effect made from a processed flexatone sound, for characters showing puzzlement or uncertainty. Includes 1 item.",
11
+ "when_to_use": "For the instant a question mark appears over a character's head to express doubt or confusion."
12
+ }
13
+ },
7
14
  "tags": [
8
15
  "sfx",
9
16
  "reaction",
@@ -4,6 +4,13 @@
4
4
  "title": "DOVA-SYNDROME 定番BGM(2:23 AM / Morning)",
5
5
  "description": "Vlog・解説の敷きに使える定番の落ち着き/ポップ系 BGM 2 点。(収録数: 2点)",
6
6
  "when_to_use": "Vlog の出だし・締め・ガジェット/IT 系の敷き(2_23_AM)、外出・ポジティブな場面(Morning)",
7
+ "i18n": {
8
+ "en": {
9
+ "title": "DOVA-SYNDROME Standard BGM (2:23 AM / Morning)",
10
+ "description": "Two standard calm and pop background tracks for vlogs and explainers. Includes 2 items.",
11
+ "when_to_use": "For vlog openings and endings or gadget and IT backgrounds (2_23_AM), and outings or upbeat scenes (Morning)."
12
+ }
13
+ },
7
14
  "tags": [
8
15
  "bgm",
9
16
  "calm",
@@ -4,6 +4,13 @@
4
4
  "title": "Freesound 鉛筆で書く音(InspectorJ)",
5
5
  "description": "鉛筆で紙に書く音を至近距離でマイクが追従して収録したリアルな筆記音(尺12.7秒)。H1 Zoomレコーダーで2016年11月1日に収録。(収録数: 1点)",
6
6
  "when_to_use": "手書き/筆記シーンの実写的な筆記音、ノート/手紙を書く描写のリアリズムを足したいとき",
7
+ "i18n": {
8
+ "en": {
9
+ "title": "Freesound Pencil Writing (InspectorJ)",
10
+ "description": "Realistic close-miked pencil writing on paper, with the microphone following the pencil. Duration: 12.7 seconds. Recorded on a Zoom H1 on November 1, 2016. Includes 1 item.",
11
+ "when_to_use": "For realistic writing sounds in live-action handwriting scenes and added detail when someone writes in a notebook or letter."
12
+ }
13
+ },
7
14
  "tags": [
8
15
  "sfx",
9
16
  "writing",
@@ -4,6 +4,13 @@
4
4
  "title": "インパクト SFX パック",
5
5
  "description": "衝突・打撃・フォーリー系の効果音 130 点セット。テロップの強調出現やカット切り替え、アクセントを付けたい瞬間に使える短尺の衝撃音が中心",
6
6
  "when_to_use": "強調テロップの出現・カット切り替え・ビートに合わせたアクセント演出など、映像に「ドン」「ガツン」といった衝撃感を足したいシーン",
7
+ "i18n": {
8
+ "en": {
9
+ "title": "Impact SFX Pack",
10
+ "description": "A set of 130 collision, hit, and Foley sound effects, mostly short impacts for emphasized caption entrances, cut changes, and moments needing an accent.",
11
+ "when_to_use": "For emphasized caption entrances, cut changes, and beat-aligned accents that need a thump or sharp impact."
12
+ }
13
+ },
7
14
  "tags": ["sfx", "impact", "foley", "transition", "accent"],
8
15
  "knobs": [],
9
16
  "ai_usage": "取得後は音源をそのまま edit.json のカット点・強調ポイントに合わせて配置する用途を想定。音源そのもの(波形)は改変しない。音量調整はクリップ側のパラメータで行う",
@@ -4,6 +4,13 @@
4
4
  "title": "魔王魂 ネオロック(Burn_Place / Speed_King / Desert_journey)",
5
5
  "description": "対決・闘志の演出に使える激しめロック BGM 3 点。(収録数: 3点)",
6
6
  "when_to_use": "相手と戦う・闘志を燃やす・速いテンポで畳み掛ける場面",
7
+ "i18n": {
8
+ "en": {
9
+ "title": "Maoudamashii Neo Rock (Burn_Place / Speed_King / Desert_journey)",
10
+ "description": "Three intense rock background tracks for confrontation and fighting spirit. Includes 3 items.",
11
+ "when_to_use": "For fighting an opponent, building resolve, or driving through a fast-paced sequence."
12
+ }
13
+ },
7
14
  "tags": [
8
15
  "bgm",
9
16
  "rock",
@@ -2,8 +2,15 @@
2
2
  "id": "maoudamashii-bgm-piano",
3
3
  "category": "audio",
4
4
  "title": "魔王魂 ピアノ(静寂の世界へ / 見渡す街へ)",
5
- "description": "静けさ・ゆったりした時間の流れを出すピアノ BGM 2 点。(収録数: 2点) 【audio-standard】部分参加 1/2 音(対象はローカル index.jsonl の standard フラグ / 正本は内部リポ sfx-curation.json)",
5
+ "description": "静けさ・ゆったりした時間の流れを出すピアノ BGM 2 点。(収録数: 2点) 【audio-standard】部分参加 1/2 音(対象はローカル index.jsonl の standard フラグ)",
6
6
  "when_to_use": "静寂・喪失・ぼんやりした時間(静寂の世界へ)、美しい景色をゆったり見せる場面(見渡す街へ)",
7
+ "i18n": {
8
+ "en": {
9
+ "title": "Maoudamashii Piano (Seijaku no Sekai e / Miwatasu Machi e)",
10
+ "description": "Two piano background tracks for quiet and unhurried time. Includes 2 items. Under audio-standard, 1 of 2 sounds participates; the local index.jsonl standard flag identifies the item.",
11
+ "when_to_use": "For silence, loss, or drifting time (Seijaku no Sekai e), and for lingering over beautiful scenery (Miwatasu Machi e)."
12
+ }
13
+ },
7
14
  "tags": [
8
15
  "bgm",
9
16
  "piano",