akari-video 0.1.80 → 0.1.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/assets-command.mjs +34 -0
- package/src/cli.mjs +12 -0
- package/src/narration-command.mjs +243 -17
- package/src/store-command.mjs +12 -10
- package/src/vendor-sources.mjs +14 -0
- package/vendor/.akari-capability-sources.json +2 -0
- package/vendor/docs/contract-2026-09-13-generation-v0.md +15 -1
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/akari-tools/src/world/invariants.mjs +117 -0
- package/vendor/packages/akari-tools/src/world/normalize.mjs +139 -0
- package/vendor/packages/asset-resolver/src/paid-zip.mjs +32 -12
- package/vendor/packages/asset-resolver/test/fieldreport-zip.test.mjs +24 -0
- package/vendor/packages/audio-library-setup/bin/fetch-akari-sounds.mjs +15 -61
- package/vendor/packages/audio-library-setup/test/fetch-akari-sounds.test.mjs +15 -11
- package/vendor/packages/edit-lint/src/edit-lint.mjs +54 -12
- package/vendor/packages/edit-lint/src/engine-capabilities.json +124 -0
- package/vendor/packages/edit-store/lib/audio-schedule.js +30 -0
- package/vendor/packages/edit-store/lib/caption-display.js +4 -2
- package/vendor/packages/edit-store/lib/caption-store.d.ts +8 -0
- package/vendor/packages/edit-store/lib/caption-store.js +114 -1
- package/vendor/packages/edit-store/lib/generation-meta.d.ts +3 -1
- package/vendor/packages/edit-store/lib/generation-meta.js +6 -0
- package/vendor/packages/edit-store/lib/webview-kernel.js +35 -0
- package/vendor/packages/overlay-runtime/runtimes.mjs +466 -0
- package/vendor/packages/overlay-runtime/src/fragment-source-write.d.ts +2 -0
- package/vendor/packages/overlay-runtime/src/fragment-source-write.mjs +103 -0
- package/vendor/packages/overlay-runtime/src/glass-runtime.js +657 -0
- package/vendor/packages/overlay-runtime/src/interaction.css +138 -0
- package/vendor/packages/overlay-runtime/src/interaction.js +3435 -0
- package/vendor/packages/overlay-runtime/src/keyframes.d.ts +24 -0
- package/vendor/packages/overlay-runtime/src/keyframes.mjs +176 -0
- package/vendor/packages/overlay-runtime/src/minimap.css +37 -0
- package/vendor/packages/overlay-runtime/src/minimap.js +110 -0
- package/vendor/packages/overlay-runtime/src/motion-vocab.css +47 -0
- package/vendor/packages/overlay-runtime/src/overlay-runtime.js +437 -0
- package/vendor/packages/overlay-runtime/src/parts.d.ts +40 -0
- package/vendor/packages/overlay-runtime/src/parts.mjs +527 -0
- package/vendor/packages/overlay-runtime/src/runtime-registry.js +19 -0
- package/vendor/packages/overlay-runtime/src/selection-scope.mjs +122 -0
- package/vendor/packages/overlay-runtime/src/slot-params.js +41 -0
- package/vendor/packages/overlay-runtime/src/text-split.js +202 -0
- package/vendor/packages/overlay-runtime/src/three-runtime.js +2692 -0
- package/vendor/packages/overlay-runtime/src/vendor/LICENSE.liquid-glass-js +21 -0
- package/vendor/packages/overlay-runtime/src/vendor/avatar-vrm-bundle.js +983 -0
- package/vendor/packages/overlay-runtime/src/vendor/budoux-LICENSE.txt +213 -0
- package/vendor/packages/overlay-runtime/src/vendor/budoux-ja-bundle.js +110 -0
- package/vendor/packages/overlay-runtime/src/vendor/matter-js-LICENSE.txt +21 -0
- package/vendor/packages/overlay-runtime/src/vendor/opentype.js-LICENSE.txt +20 -0
- package/vendor/packages/overlay-runtime/src/vendor/poly-decomp-LICENSE.txt +20 -0
- package/vendor/packages/overlay-runtime/src/vendor/three-LICENSE.txt +21 -0
- package/vendor/packages/overlay-runtime/src/vendor/three-bundle.js +4116 -0
- package/vendor/packages/overlay-runtime/src/vendor/three-vrm-LICENSE.txt +22 -0
- package/vendor/packages/overlay-runtime/src/vendor/troika-three-text-LICENSE.txt +22 -0
- package/vendor/packages/overlay-runtime/src/vendor/vendor-3d-text-bundle.js +248 -0
- package/vendor/packages/overlay-runtime/src/vendor/vgpu-LICENSE.txt +21 -0
- package/vendor/packages/overlay-runtime/src/vendor/vgpu-bundle.js +17 -0
- package/vendor/packages/overlay-runtime/src/vendor/world-camera.js +80 -0
- package/vendor/packages/overlay-runtime/src/vgpu-runtime.js +365 -0
- package/vendor/packages/overlay-runtime/src/video-fx.js +605 -0
- package/vendor/packages/overlay-runtime/src/viewport-units.js +148 -0
- package/vendor/packages/overlay-runtime/src/world-runtime.js +282 -0
- package/vendor/packages/overlay-runtime/test-harness/fonts/ZenKakuGothicNew-Black.ttf +0 -0
- package/vendor/packages/render-cut/src/caption-font.mjs +59 -0
- package/vendor/packages/render-cut/src/fragment-assets.mjs +212 -0
- package/vendor/packages/render-cut/src/html-scan.mjs +10 -0
- package/vendor/packages/render-cut/src/library-reference.mjs +97 -0
- package/vendor/packages/render-cut/src/render-inputs.mjs +508 -0
- package/vendor/packages/schemas/generation-meta.schema.json +1 -1
- package/vendor/packages/schemas/intake.schema.json +1 -1
- package/vendor/packages/schemas/test/generation-meta.test.mjs +9 -0
- package/vendor/packages/word-book/src/index.mjs +530 -0
- package/vendor/skills/address-review/bin/core/review-store.mjs +73 -39
- package/vendor/skills/address-review/test/review-loss.test.mjs +41 -0
- package/vendor/skills/compile-review-session/bin/core/install-root.mjs +48 -0
- package/vendor/skills/compile-review-session/bin/core/review-store.mjs +97 -23
- package/vendor/skills/compile-review-session/bin/core/time-mapping.mjs +56 -12
- package/vendor/skills/compile-review-session/dev-fixtures/issue-69-review-json/README.md +54 -0
- package/vendor/skills/compile-review-session/dev-fixtures/issue-69-review-json/reproduce.mjs +58 -0
- package/vendor/skills/compile-review-session/dev-fixtures/issue-79/README.md +3 -0
- package/vendor/skills/compile-review-session/dev-fixtures/issue-79/reproduce.mjs +9 -0
- package/vendor/skills/compile-review-session/test/helpers/issue-79-fixture.mjs +93 -0
- package/vendor/skills/compile-review-session/test/issue-79-snapshot.test.mjs +71 -0
- package/vendor/skills/compile-review-session/test/review-loss.test.mjs +52 -0
- package/vendor/skills/edit-lint/SKILL.md +2 -0
- package/vendor/skills/generate-narration/SKILL.md +1 -1
- package/vendor/skills/generate-narration/engines.md +19 -4
- package/vendor/skills/manage-connections/bin/doctor.mjs +3 -1
- package/vendor/skills/manage-connections/test/fieldreport-report-location.test.mjs +24 -0
- package/vendor/skills/overlay-authoring/motion.md +6 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.81",
|
|
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/assets-command.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
2
4
|
|
|
3
5
|
import { resolveLauncherAssets } from './repo-assets.mjs';
|
|
4
6
|
import { assetsResolverUnavailableError } from './messages.mjs';
|
|
@@ -29,6 +31,19 @@ export async function runAssetsCommand(args, options = {}) {
|
|
|
29
31
|
const env = options.env ?? process.env;
|
|
30
32
|
const assets = options.assets ?? resolveLauncherAssets();
|
|
31
33
|
|
|
34
|
+
// These catalog pack IDs are reference entries; their tracks are distributed by the
|
|
35
|
+
// first-party Sounds fetcher, while the resolver catalog contains only individual IDs.
|
|
36
|
+
if (args[0] === 'fetch' && /^akari-sounds-[a-z0-9-]+$/u.test(args[1] ?? '')) {
|
|
37
|
+
if (!assets?.audioFetchScriptPath) {
|
|
38
|
+
logError('AKARI Sounds の取得スクリプトが同梱されていません');
|
|
39
|
+
return { exitCode: 1 };
|
|
40
|
+
}
|
|
41
|
+
const spawn = options.spawnSoundsCli ?? defaultSpawnAssetsCli;
|
|
42
|
+
const fetchArgs = ['--pack', args[1], ...(args.includes('--force') ? ['--force'] : [])];
|
|
43
|
+
const result = spawn(assets.audioFetchScriptPath, fetchArgs, env);
|
|
44
|
+
return { exitCode: typeof result.status === 'number' ? result.status : 1 };
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
const cliPath = assets?.assetResolverCliPath;
|
|
33
48
|
if (!cliPath) {
|
|
34
49
|
logError(assetsResolverUnavailableError());
|
|
@@ -36,6 +51,25 @@ export async function runAssetsCommand(args, options = {}) {
|
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
const spawnAssetsCli = options.spawnAssetsCli ?? defaultSpawnAssetsCli;
|
|
54
|
+
if (args[0] === 'fetch' && assets?.repoRoot) {
|
|
55
|
+
try {
|
|
56
|
+
const load = options.loadCatalog ?? (await import(pathToFileURL(resolve(dirname(cliPath), '../src/catalog.mjs')).href)).loadCatalog;
|
|
57
|
+
const catalog = await load({ env });
|
|
58
|
+
if (!catalog.items.some(item => item.id === args[1])) {
|
|
59
|
+
const members = catalog.items.filter(item => Array.isArray(item.tags) && item.tags.includes(`pack:${args[1]}`));
|
|
60
|
+
if (members.length > 0) {
|
|
61
|
+
let exitCode = 0;
|
|
62
|
+
for (const item of members) {
|
|
63
|
+
const result = spawnAssetsCli(cliPath, ['fetch', item.id, ...args.slice(2)], env);
|
|
64
|
+
if (result.status !== 0) exitCode = 1;
|
|
65
|
+
}
|
|
66
|
+
return { exitCode };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Delegate to the resolver, which owns catalog errors and offline guidance.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
39
73
|
const result = spawnAssetsCli(cliPath, args, env);
|
|
40
74
|
const exitCode = typeof result.status === 'number' ? result.status : (result.error ? 1 : 0);
|
|
41
75
|
return { exitCode };
|
package/src/cli.mjs
CHANGED
|
@@ -75,6 +75,13 @@ export async function run(args, options = {}) {
|
|
|
75
75
|
);
|
|
76
76
|
|
|
77
77
|
let projectRoot = options.projectRoot ?? process.cwd();
|
|
78
|
+
const bareNonInteractive = args.length === 0
|
|
79
|
+
&& !(options.isTTY ?? Boolean(process.stdin.isTTY && process.stdout.isTTY));
|
|
80
|
+
if (bareNonInteractive && !detectProjectState(projectRoot).scaffolded) {
|
|
81
|
+
log(`このフォルダーは AKARI Video プロジェクトとしてまだセットアップされていません: ${projectRoot}`);
|
|
82
|
+
log('非対話シェルのため状態確認のみで終了します。');
|
|
83
|
+
return { exitCode: 0, scaffolded: false, claudeLaunched: false, opencodeLaunched: false };
|
|
84
|
+
}
|
|
78
85
|
|
|
79
86
|
// 作業場(creator-root)の初回動線(契約 §5・§6-1)。`--here` はお試しモード強制
|
|
80
87
|
// (現行動作)のため丸ごとスキップする(契約 §9・非 TTY と同じ現行動作互換の扱い)。
|
|
@@ -125,6 +132,11 @@ export async function run(args, options = {}) {
|
|
|
125
132
|
log(describeVersionStatus(versionInfo, readCacheSync(resolveCachePath(env)), runtimeDiagnostics));
|
|
126
133
|
}
|
|
127
134
|
|
|
135
|
+
if (bareNonInteractive) {
|
|
136
|
+
log('非対話シェルのため状態確認のみで終了します。');
|
|
137
|
+
return { exitCode: 0, scaffolded: state.scaffolded, claudeLaunched: false, opencodeLaunched: false };
|
|
138
|
+
}
|
|
139
|
+
|
|
128
140
|
// 新版通知(契約 §4-1): キャッシュの読み比較のみ・ネットワークには一切触れない
|
|
129
141
|
// (起動をブロックしない)。fetch は detached な子プロセスへ切り離し、
|
|
130
142
|
// 結果は次回セッションで効く。
|
|
@@ -5,7 +5,8 @@ import fs from "node:fs";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { spawn, spawnSync } from "node:child_process";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
|
+
import { resolveLauncherAssets } from "./repo-assets.mjs";
|
|
9
10
|
|
|
10
11
|
const VOICEVOX_BASE_URL = "http://127.0.0.1:50021";
|
|
11
12
|
const VOICEVOX_RUN_ENV = "VOICEVOX_RUN";
|
|
@@ -14,6 +15,28 @@ const FAL_TTS_URL = "https://fal.run/fal-ai/qwen-3-tts/text-to-speech/1.7b";
|
|
|
14
15
|
const FAL_USD_PER_1000_CHARS = 0.09;
|
|
15
16
|
const GEMINI_TTS_URL = "https://fal.run/fal-ai/gemini-tts";
|
|
16
17
|
const GEMINI_USD_PER_1000_CHARS = 0.05;
|
|
18
|
+
const IRODORI_DEFAULT_URL = "http://127.0.0.1:8088";
|
|
19
|
+
const IRODORI_SETUP_URL = "https://github.com/Aratako/Irodori-TTS-Server";
|
|
20
|
+
const IRODORI_RECIPES = [
|
|
21
|
+
{ id: "narrator-male", label: "落ち着いた男性ナレーター", caption: "落ち着いた低めの男性の声。聞き取りやすく、ナレーションのように丁寧に話す。", default: true },
|
|
22
|
+
{ id: "bright-female", label: "明るい若い女性", caption: "明るく元気な若い女性の声。はきはきと楽しそうに話す。" },
|
|
23
|
+
{ id: "slow-explainer", label: "低くゆっくりした解説", caption: "低めで落ち着いた声。ゆっくり、一語ずつ丁寧に説明する。" },
|
|
24
|
+
];
|
|
25
|
+
function irodoriEndpoint(value, env = process.env) {
|
|
26
|
+
const raw = value ?? env.AKARI_IRODORI_URL ?? IRODORI_DEFAULT_URL;
|
|
27
|
+
let url;
|
|
28
|
+
try { url = new URL(raw); } catch { throw new PublicError("--irodori-url は http または https の URL にしてください", 2); }
|
|
29
|
+
if (!["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password || url.search || url.hash) {
|
|
30
|
+
throw new PublicError("--irodori-url は http または https のサーバー URL にしてください", 2);
|
|
31
|
+
}
|
|
32
|
+
return { base: `${url.origin}${url.pathname.replace(/\/+$/, "")}`, server: `${url.hostname}:${url.port || (url.protocol === "https:" ? "443" : "80")}`,
|
|
33
|
+
network: !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname) };
|
|
34
|
+
}
|
|
35
|
+
function irodoriTimeout(env = process.env) {
|
|
36
|
+
const value = Number(env.AKARI_IRODORI_TIMEOUT_MS ?? 600_000);
|
|
37
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new PublicError("AKARI_IRODORI_TIMEOUT_MS は正の整数にしてください", 2);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
17
40
|
const GEMINI_VOICES = Object.entries({
|
|
18
41
|
Leda: "Youthful", Achernar: "Soft", Achird: "Friendly", Algenib: "Gravelly",
|
|
19
42
|
Algieba: "Smooth", Alnilam: "Firm", Aoede: "Breezy", Autonoe: "Bright",
|
|
@@ -29,11 +52,11 @@ const GEMINI_VOICES = Object.entries({
|
|
|
29
52
|
const usage = [
|
|
30
53
|
"使い方:",
|
|
31
54
|
" akari narration generate \\",
|
|
32
|
-
" --project <projectDir> --engine <voicevox|gemini-tts|fal-qwen3> \\",
|
|
55
|
+
" --project <projectDir> --engine <voicevox|gemini-tts|irodori|fal-qwen3> \\",
|
|
33
56
|
" (--reading-file <読み原稿.txt> | --text <原稿>) [--script-file <表示原稿.txt>] \\",
|
|
34
57
|
" [--t <タイムライン秒>] [--gain-db 0] [--id n-0001] \\",
|
|
35
58
|
" [--speaker 3] [--voice Leda] [--style <text>] [--speed 1] \\",
|
|
36
|
-
" [--profile owner-ja] [--caption-ref c-0001] [--dry-run] [--yes] [--apply] [--json]",
|
|
59
|
+
" [--profile owner-ja] [--irodori-url http://127.0.0.1:8088] [--caption-ref c-0001] [--dry-run] [--yes] [--apply] [--json]",
|
|
37
60
|
].join("\n");
|
|
38
61
|
const commandUsage = [
|
|
39
62
|
"使い方: akari narration <subcommand> [options]",
|
|
@@ -44,9 +67,144 @@ const commandUsage = [
|
|
|
44
67
|
" start VOICEVOX エンジンを起動する",
|
|
45
68
|
" stop AKARI が起動した VOICEVOX エンジンを止める",
|
|
46
69
|
" voices 声一覧を表示する",
|
|
70
|
+
" verify 生成音声をこの Mac で聞き取り、字幕と照合する",
|
|
47
71
|
"",
|
|
48
72
|
usage,
|
|
49
73
|
].join("\n");
|
|
74
|
+
const verifyUsage = "使い方: akari narration verify --project <root> (--id <n-NNNN> | --audio <path> --text <字幕の文字>) [--reading <読み原稿>] [--backend auto|speechanalyzer|whisper] [--record <dir>] --json";
|
|
75
|
+
export const VERIFY_THRESHOLDS = Object.freeze({ ok: 0.9, check: 0.7 });
|
|
76
|
+
|
|
77
|
+
export function normalizeVerifyText(value) {
|
|
78
|
+
return String(value ?? "").normalize("NFKC").toLowerCase().replace(/[\p{P}\p{S}\s]/gu, "");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function verifyLanguage(expected) {
|
|
82
|
+
return /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/u.test(String(expected ?? "").normalize("NFKC")) ? "ja" : "auto";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function compareNarrationText(expected, heard) {
|
|
86
|
+
const a = [...normalizeVerifyText(expected)], b = [...normalizeVerifyText(heard)];
|
|
87
|
+
const dp = Array.from({ length: a.length + 1 }, () => new Uint32Array(b.length + 1));
|
|
88
|
+
for (let i = 0; i <= a.length; i++) dp[i][0] = i;
|
|
89
|
+
for (let j = 0; j <= b.length; j++) dp[0][j] = j;
|
|
90
|
+
for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) {
|
|
91
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
92
|
+
}
|
|
93
|
+
const operations = [];
|
|
94
|
+
let i = a.length, j = b.length;
|
|
95
|
+
while (i || j) {
|
|
96
|
+
if (i && j && a[i - 1] === b[j - 1] && dp[i][j] === dp[i - 1][j - 1]) {
|
|
97
|
+
operations.push({ equal: true }); i--; j--;
|
|
98
|
+
} else if (i && j && dp[i][j] === dp[i - 1][j - 1] + 1) {
|
|
99
|
+
operations.push({ expected: a[--i], heard: b[--j] });
|
|
100
|
+
} else if (i && dp[i][j] === dp[i - 1][j] + 1) {
|
|
101
|
+
operations.push({ expected: a[--i], heard: "" });
|
|
102
|
+
} else {
|
|
103
|
+
operations.push({ expected: "", heard: b[--j] });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const diffs = [];
|
|
107
|
+
let current;
|
|
108
|
+
for (const operation of operations.reverse()) {
|
|
109
|
+
if (operation.equal) { if (current) diffs.push(current); current = null; }
|
|
110
|
+
else {
|
|
111
|
+
current ??= { expected: "", heard: "" };
|
|
112
|
+
current.expected += operation.expected; current.heard += operation.heard;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (current) diffs.push(current);
|
|
116
|
+
const rawScore = a.length || b.length ? 1 - dp[a.length][b.length] / Math.max(a.length, b.length) : 1;
|
|
117
|
+
const score = Number(rawScore.toFixed(3));
|
|
118
|
+
return { score, verdict: score >= VERIFY_THRESHOLDS.ok ? "ok" : score >= VERIFY_THRESHOLDS.check ? "check" : "ng", diffs };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseVerifyArguments(args) {
|
|
122
|
+
const options = { backend: "auto", reading: null, record: null, checkBackend: false };
|
|
123
|
+
const keys = new Set(["--project", "--id", "--audio", "--text", "--reading", "--backend", "--record"]);
|
|
124
|
+
for (let index = 1; index < args.length; index++) {
|
|
125
|
+
const flag = args[index];
|
|
126
|
+
if (flag === "--json" || flag === "--check-backend") { if (flag === "--check-backend") options.checkBackend = true; continue; }
|
|
127
|
+
if (!keys.has(flag) || !args[index + 1] || args[index + 1].startsWith("--")) throw new PublicError(`引数が不正です: ${flag}\n${verifyUsage}`, 2);
|
|
128
|
+
options[flag.slice(2)] = args[++index];
|
|
129
|
+
}
|
|
130
|
+
if (!options.project || !["auto", "speechanalyzer", "whisper"].includes(options.backend)) throw new PublicError(verifyUsage, 2);
|
|
131
|
+
if (!options.checkBackend && ((!!options.id) === (!!options.audio)) ) throw new PublicError(verifyUsage, 2);
|
|
132
|
+
if (options.id && !/^n-\d{4}$/.test(options.id)) throw new PublicError("--id は n- に続く 4 桁です", 2);
|
|
133
|
+
if (options.audio && options.text === undefined) throw new PublicError("--audio には --text が必要です", 2);
|
|
134
|
+
return options;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resolveTranscribeModulePath(runtime = {}) {
|
|
138
|
+
const assets = (runtime.resolveLauncherAssets ?? resolveLauncherAssets)();
|
|
139
|
+
const relative = path.join("packages", "akari-tools", "src", "media", "transcribe.mjs");
|
|
140
|
+
const candidates = [path.join(assets.repoRoot, relative),
|
|
141
|
+
assets.mediaScript ? path.resolve(path.dirname(assets.mediaScript), "..", "src", "media", "transcribe.mjs") : null,
|
|
142
|
+
path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "vendor", relative)];
|
|
143
|
+
return candidates.find(candidate => candidate && fs.existsSync(candidate)) ?? null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function loadTranscribeModule(runtime = {}) {
|
|
147
|
+
const modulePath = (runtime.resolveTranscribeModulePath ?? resolveTranscribeModulePath)(runtime);
|
|
148
|
+
if (!modulePath) throw new PublicError("ローカル文字起こしの実装が同梱されていません", 3);
|
|
149
|
+
try { return await import(pathToFileURL(modulePath).href); }
|
|
150
|
+
catch { throw new PublicError("ローカル文字起こしの実装を読み込めません", 3); }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function verifyBackend(requested, runtime, transcribe) {
|
|
154
|
+
const speech = (runtime.speechAnalyzerAvailable ?? transcribe.speechAnalyzerAvailable)();
|
|
155
|
+
const whisper = (runtime.resolveWhisper ?? transcribe.resolveWhisper)();
|
|
156
|
+
if (requested === "speechanalyzer" && speech || requested === "whisper" && whisper || requested === "auto" && (speech || whisper)) {
|
|
157
|
+
return requested === "auto" ? speech ? "speech-analyzer" : "whisper-cpp" : requested === "whisper" ? "whisper-cpp" : "speech-analyzer";
|
|
158
|
+
}
|
|
159
|
+
throw new PublicError(requested === "speechanalyzer" ? "SpeechAnalyzer を利用できません" : requested === "whisper" ? "whisper.cpp の実行ファイルまたはモデルが見つかりません" : "この Mac に利用できる文字起こし backend がありません(SpeechAnalyzer / whisper.cpp)", 3);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function runVerify(options, io, runtime = {}) {
|
|
163
|
+
let backend;
|
|
164
|
+
let transcribe;
|
|
165
|
+
try {
|
|
166
|
+
transcribe = await (runtime.loadTranscribeModule ?? loadTranscribeModule)(runtime);
|
|
167
|
+
backend = verifyBackend(options.backend, runtime, transcribe);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (error instanceof PublicError && error.exitCode === 3) { printCompactJson({ status: "unavailable", reason: error.message }, io.log); return 3; }
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
if (options.checkBackend) { printCompactJson({ status: "ok", backend }, io.log); return 0; }
|
|
174
|
+
const root = path.resolve(options.project);
|
|
175
|
+
let entry;
|
|
176
|
+
if (options.id) {
|
|
177
|
+
const edit = JSON.parse(fs.readFileSync(path.join(root, "edit.json"), "utf8"));
|
|
178
|
+
entry = edit.audio?.narration?.find(item => item.id === options.id);
|
|
179
|
+
if (!entry?.path || typeof entry.script !== "string") throw new PublicError(`音声 ${options.id} または字幕の文字が見つかりません`, 2);
|
|
180
|
+
}
|
|
181
|
+
const expected = entry?.script ?? options.text;
|
|
182
|
+
const audio = path.resolve(root, entry?.path ?? options.audio);
|
|
183
|
+
if (entry && !audio.startsWith(root + path.sep)) throw new PublicError("音声パスがプロジェクト外です", 2);
|
|
184
|
+
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "akari-narration-verify-"));
|
|
185
|
+
const started = performance.now();
|
|
186
|
+
try {
|
|
187
|
+
const copy = path.join(temporary, path.basename(audio));
|
|
188
|
+
fs.copyFileSync(audio, copy);
|
|
189
|
+
const transcript = await (runtime.transcribeMedia ?? transcribe.transcribeMedia)(copy, {
|
|
190
|
+
cwd: temporary, noRecord: true, wordBook: false, lang: verifyLanguage(expected),
|
|
191
|
+
...(options.backend === "auto" ? {} : { backend }),
|
|
192
|
+
});
|
|
193
|
+
const heard = (transcript.segments ?? []).map(segment => segment.text ?? "").join("").trim();
|
|
194
|
+
const comparison = compareNarrationText(expected, heard);
|
|
195
|
+
const result = { version: 1, status: "ok", id: options.id ?? null, ...comparison, expected, heard,
|
|
196
|
+
...(options.reading !== null ? { reading: options.reading } : {}),
|
|
197
|
+
backend: transcript.backend ?? backend, elapsed_s: Number(((performance.now() - started) / 1000).toFixed(3)) };
|
|
198
|
+
if (options.record) {
|
|
199
|
+
fs.mkdirSync(options.record, { recursive: true });
|
|
200
|
+
fs.appendFileSync(path.join(options.record, "narration-verify.jsonl"), `${JSON.stringify({ expected, reading: options.reading ?? entry?.reading ?? null,
|
|
201
|
+
heard, score: result.score, verdict: result.verdict, engine: entry?.provenance?.engine ?? null,
|
|
202
|
+
voice: entry?.provenance?.voice ?? null, backend: result.backend, generated_at: new Date().toISOString() })}\n`);
|
|
203
|
+
}
|
|
204
|
+
printCompactJson(result, io.log);
|
|
205
|
+
return 0;
|
|
206
|
+
} finally { fs.rmSync(temporary, { recursive: true, force: true }); }
|
|
207
|
+
}
|
|
50
208
|
|
|
51
209
|
class PublicError extends Error {
|
|
52
210
|
constructor(message, exitCode = 1) {
|
|
@@ -75,7 +233,7 @@ function isFiniteNumber(value) {
|
|
|
75
233
|
const VALUE_OPTIONS = new Set([
|
|
76
234
|
"--project", "--engine", "--reading-file", "--script-file",
|
|
77
235
|
"--t", "--gain-db", "--id", "--speaker", "--profile",
|
|
78
|
-
"--voice", "--style", "--speed", "--text", "--caption-ref",
|
|
236
|
+
"--voice", "--style", "--speed", "--text", "--caption-ref", "--irodori-url",
|
|
79
237
|
]);
|
|
80
238
|
const FLAG_OPTIONS = new Set(["--dry-run", "--yes", "--apply", "--json"]);
|
|
81
239
|
|
|
@@ -93,7 +251,7 @@ function parseArguments(argv) {
|
|
|
93
251
|
id: null,
|
|
94
252
|
speaker: "3",
|
|
95
253
|
profile: null,
|
|
96
|
-
voice:
|
|
254
|
+
voice: null, style: null, speed: null, text: null, captionRef: null, irodoriUrl: null, json: false,
|
|
97
255
|
dryRun: false,
|
|
98
256
|
yes: false,
|
|
99
257
|
apply: false,
|
|
@@ -121,6 +279,7 @@ function parseArguments(argv) {
|
|
|
121
279
|
case "--speed": options.speed = Number(value); break;
|
|
122
280
|
case "--text": options.text = value; break;
|
|
123
281
|
case "--caption-ref": options.captionRef = value; break;
|
|
282
|
+
case "--irodori-url": options.irodoriUrl = value; break;
|
|
124
283
|
default: break;
|
|
125
284
|
}
|
|
126
285
|
} else if (argument === "--dry-run") {
|
|
@@ -137,15 +296,22 @@ function parseArguments(argv) {
|
|
|
137
296
|
}
|
|
138
297
|
|
|
139
298
|
if (!options.project) throw new PublicError("--project が必要です");
|
|
140
|
-
if (!["voicevox", "gemini-tts", "fal-qwen3"].includes(options.engine)) {
|
|
141
|
-
throw new PublicError("--engine には voicevox、gemini-tts または fal-qwen3 を指定してください", 2);
|
|
299
|
+
if (!["voicevox", "gemini-tts", "irodori", "fal-qwen3"].includes(options.engine)) {
|
|
300
|
+
throw new PublicError("--engine には voicevox、gemini-tts、irodori または fal-qwen3 を指定してください", 2);
|
|
142
301
|
}
|
|
302
|
+
if (options.engine === "irodori") {
|
|
303
|
+
options.voice ??= "narrator-male";
|
|
304
|
+
if (!IRODORI_RECIPES.some(recipe => recipe.id === options.voice) && options.voice !== "custom") throw new PublicError("彩の声レシピが不明です", 2);
|
|
305
|
+
if (options.voice === "custom" && !options.style?.trim()) throw new PublicError("自分で書く声には --style が必要です", 2);
|
|
306
|
+
options.irodori = irodoriEndpoint(options.irodoriUrl);
|
|
307
|
+
irodoriTimeout();
|
|
308
|
+
} else options.voice ??= "Leda";
|
|
143
309
|
if (!options.readingFile && !options.text) throw new PublicError("--reading-file または --text が必要です");
|
|
144
310
|
if (options.engine === "gemini-tts" && !GEMINI_VOICES.some(({ id }) => id === options.voice)) {
|
|
145
311
|
throw new PublicError(`--voice に使える声: ${GEMINI_VOICES.map(({ id }) => id).join(", ")}`, 2);
|
|
146
312
|
}
|
|
147
|
-
if (options.speed !== null && (!isFiniteNumber(options.speed) || options.speed < 0.5 || options.speed > 2)) {
|
|
148
|
-
throw new PublicError("--speed は 0.5 から 2.0 の範囲で指定してください", 2);
|
|
313
|
+
if (options.speed !== null && (!isFiniteNumber(options.speed) || options.speed < (options.engine === "irodori" ? 0.25 : 0.5) || options.speed > (options.engine === "irodori" ? 4 : 2))) {
|
|
314
|
+
throw new PublicError(options.engine === "irodori" ? "--speed は 0.25 から 4.0 の範囲で指定してください" : "--speed は 0.5 から 2.0 の範囲で指定してください", 2);
|
|
149
315
|
}
|
|
150
316
|
if (options.captionRef !== null && !/^c-\d{4}$/.test(options.captionRef)) {
|
|
151
317
|
throw new PublicError("--caption-ref は c- に続く 4 桁の数字で指定してください", 2);
|
|
@@ -203,7 +369,7 @@ function computeNextId(projectDir) {
|
|
|
203
369
|
}
|
|
204
370
|
|
|
205
371
|
function extensionFor(engine) {
|
|
206
|
-
return
|
|
372
|
+
return ["voicevox", "irodori"].includes(engine) ? "wav" : "mp3";
|
|
207
373
|
}
|
|
208
374
|
|
|
209
375
|
function relativeOutputPath(id, engine) {
|
|
@@ -612,6 +778,30 @@ function maskKey(secret) {
|
|
|
612
778
|
return secret ? "***configured***" : "***unconfigured***";
|
|
613
779
|
}
|
|
614
780
|
|
|
781
|
+
async function probeIrodori(endpoint, fetchImpl = fetch) {
|
|
782
|
+
try {
|
|
783
|
+
const response = await fetchImpl(`${endpoint.base}/health`, { signal: AbortSignal.timeout(2000) });
|
|
784
|
+
return response.ok;
|
|
785
|
+
} catch { return false; }
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
async function synthesizeIrodori(readingText, options, fetchImpl = fetch) {
|
|
789
|
+
const caption = options.style?.trim() || IRODORI_RECIPES.find(recipe => recipe.id === options.voice)?.caption;
|
|
790
|
+
let response;
|
|
791
|
+
try {
|
|
792
|
+
response = await fetchImpl(`${options.irodori.base}/v1/audio/speech`, {
|
|
793
|
+
method: "POST", headers: { "Content-Type": "application/json" }, signal: AbortSignal.timeout(irodoriTimeout()),
|
|
794
|
+
body: JSON.stringify({ model: "irodori-tts", input: readingText, voice: "none", response_format: "wav",
|
|
795
|
+
speed: options.speed ?? 1, irodori: { caption } }),
|
|
796
|
+
});
|
|
797
|
+
} catch (error) { throw new PublicError(`彩サーバーに接続できません: ${error?.message ?? error}`); }
|
|
798
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
799
|
+
if (!response.ok || buffer.toString("ascii", 0, 4) !== "RIFF" || buffer.toString("ascii", 8, 12) !== "WAVE") {
|
|
800
|
+
throw new PublicError(`彩サーバーが音声を返しませんでした(HTTP ${response.status}): ${buffer.toString("utf8", 0, 300)}`);
|
|
801
|
+
}
|
|
802
|
+
return buffer;
|
|
803
|
+
}
|
|
804
|
+
|
|
615
805
|
async function listEngines(runtime = {}) {
|
|
616
806
|
let voicevox;
|
|
617
807
|
const state = await probeVoicevox(runtime.fetchImpl || fetch);
|
|
@@ -635,15 +825,27 @@ async function listEngines(runtime = {}) {
|
|
|
635
825
|
: { state: "unconfigured", label: "fal の鍵を登録" };
|
|
636
826
|
const profilesDir = path.join(os.homedir(), ".config", "akari-video", "voice-profiles");
|
|
637
827
|
const hasProfiles = fs.existsSync(profilesDir) && fs.readdirSync(profilesDir, { withFileTypes: true }).some((entry) => entry.isDirectory());
|
|
828
|
+
let endpoint;
|
|
829
|
+
try { endpoint = irodoriEndpoint(runtime.irodoriUrl, runtime.env || process.env); }
|
|
830
|
+
catch (error) { if (!(error instanceof PublicError)) throw error; }
|
|
831
|
+
const irodoriAvailable = endpoint ? await probeIrodori(endpoint, runtime.fetchImpl || fetch) : false;
|
|
638
832
|
return { version: 1, engines: [
|
|
639
833
|
{ id: "voicevox", label: "VOICEVOX", place: "local", price: { usd_per_1000_chars: 0, verified: true }, availability: voicevox, credit_required: true, supports: { speed: true, style: false } },
|
|
640
834
|
{ id: "gemini-tts", label: "Gemini 2.5 Flash TTS", place: "cloud", provider: "fal", price: { usd_per_1000_chars: GEMINI_USD_PER_1000_CHARS, verified: false, as_of: "2026-09-22" }, availability: falAvailability, default_voice: "Leda", credit_required: false, supports: { speed: false, style: true } },
|
|
641
|
-
{ id: "irodori", label: "彩(Irodori-TTS)", place: "
|
|
835
|
+
{ id: "irodori", label: "彩(Irodori-TTS)", place: endpoint?.network ? "network" : "local", experimental: true,
|
|
836
|
+
price: { usd_per_1000_chars: 0, verified: true }, credit_required: false, default_voice: "narrator-male",
|
|
837
|
+
supports: { speed: true, style: true }, availability: !endpoint
|
|
838
|
+
? { state: "unconfigured", label: "接続先 URL が正しくありません(お試し)", detail: { setup_url: IRODORI_SETUP_URL } }
|
|
839
|
+
: irodoriAvailable
|
|
840
|
+
? { state: "available", label: "お試し · 接続済み", detail: { url: endpoint.server } }
|
|
841
|
+
: { state: "unconfigured", label: "Irodori サーバーにつながりません(お試し)", detail: { setup_url: IRODORI_SETUP_URL } } },
|
|
642
842
|
{ id: "fal-qwen3", label: "fal Qwen3-TTS", place: "cloud", provider: "fal", price: { usd_per_1000_chars: FAL_USD_PER_1000_CHARS, verified: false }, availability: !configured ? falAvailability : hasProfiles ? { state: "available", label: "声プロファイルを使用できます" } : { state: "needs", label: "声プロファイルを作成" }, credit_required: false, supports: { speed: false, style: false } },
|
|
643
843
|
] };
|
|
644
844
|
}
|
|
645
845
|
|
|
646
846
|
async function listVoices(engine) {
|
|
847
|
+
if (engine === "irodori") return [...IRODORI_RECIPES.map(({ id, label, default: isDefault }) => ({ id, label, ...(isDefault ? { default: true } : {}) })),
|
|
848
|
+
{ id: "custom", label: "自分で書く(声の指示)" }];
|
|
647
849
|
if (engine === "gemini-tts") return GEMINI_VOICES;
|
|
648
850
|
if (engine === "voicevox") {
|
|
649
851
|
const handle = await ensureVoicevoxEngine();
|
|
@@ -668,13 +870,15 @@ async function listVoices(engine) {
|
|
|
668
870
|
|
|
669
871
|
function parseListArguments(args) {
|
|
670
872
|
let engine = null;
|
|
873
|
+
let irodoriUrl = null;
|
|
671
874
|
for (let index = 1; index < args.length; index += 1) {
|
|
672
875
|
if (args[index] === "--json") continue;
|
|
673
876
|
if (args[index] === "--engine" && args[index + 1]) { engine = args[++index]; continue; }
|
|
877
|
+
if (args[index] === "--irodori-url" && args[index + 1]) { irodoriUrl = args[++index]; continue; }
|
|
674
878
|
throw new PublicError(`不明な引数です: ${args[index]}`, 2);
|
|
675
879
|
}
|
|
676
880
|
if (args[0] === "voices" && !engine) throw new PublicError("voices には --engine が必要です", 2);
|
|
677
|
-
return engine;
|
|
881
|
+
return { engine, irodoriUrl };
|
|
678
882
|
}
|
|
679
883
|
|
|
680
884
|
async function runDryRun(options, readingText, io) {
|
|
@@ -708,6 +912,13 @@ async function runDryRun(options, readingText, io) {
|
|
|
708
912
|
}, io.log);
|
|
709
913
|
return;
|
|
710
914
|
}
|
|
915
|
+
if (options.engine === "irodori") {
|
|
916
|
+
emit({ dry_run: true, engine: options.engine, output_path: outputPath, estimated_cost_usd: 0,
|
|
917
|
+
request: { endpoint: `${options.irodori.base}/v1/audio/speech`, body: { model: "irodori-tts", input: readingText,
|
|
918
|
+
voice: "none", response_format: "wav", speed: options.speed ?? 1,
|
|
919
|
+
irodori: { caption: options.style?.trim() || IRODORI_RECIPES.find(recipe => recipe.id === options.voice)?.caption } } } }, io.log);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
711
922
|
const falKey = resolveFalKey();
|
|
712
923
|
const meta = readProfileMeta(options.profile);
|
|
713
924
|
const payload = buildFalPayload(readingText, meta);
|
|
@@ -726,7 +937,7 @@ async function runDryRun(options, readingText, io) {
|
|
|
726
937
|
}
|
|
727
938
|
|
|
728
939
|
function durationForAudio(buffer, outputPath, engine, warnings) {
|
|
729
|
-
if (
|
|
940
|
+
if (["voicevox", "irodori"].includes(engine)) {
|
|
730
941
|
if (buffer.length >= 44 && buffer.toString("ascii", 0, 4) === "RIFF") {
|
|
731
942
|
const bytesPerSecond = buffer.readUInt32LE(28);
|
|
732
943
|
const dataOffset = buffer.indexOf("data", 36, "ascii");
|
|
@@ -763,7 +974,7 @@ async function runGenerate(options, io) {
|
|
|
763
974
|
let provenance;
|
|
764
975
|
let costUsd = 0;
|
|
765
976
|
const warnings = [];
|
|
766
|
-
if (options.speed !== null && options.engine
|
|
977
|
+
if (options.speed !== null && !["voicevox", "irodori"].includes(options.engine)) {
|
|
767
978
|
const warning = `${options.engine} は --speed に対応していないため無視しました`;
|
|
768
979
|
warnings.push(warning);
|
|
769
980
|
io.logError(warning);
|
|
@@ -791,6 +1002,10 @@ async function runGenerate(options, io) {
|
|
|
791
1002
|
} finally {
|
|
792
1003
|
if (engineHandle.startedByUs) await stopVoicevoxEngine(engineHandle.child);
|
|
793
1004
|
}
|
|
1005
|
+
} else if (options.engine === "irodori") {
|
|
1006
|
+
audioBuffer = await synthesizeIrodori(readingText, options);
|
|
1007
|
+
provenance = { provider: "irodori", engine: "irodori-tts-v4-small", voice: options.style?.trim() ? "caption:custom" : `recipe:${options.voice}`,
|
|
1008
|
+
generated_at: new Date().toISOString(), experimental: true, server: options.irodori.server };
|
|
794
1009
|
} else {
|
|
795
1010
|
const estimatedCostUsd = options.engine === "gemini-tts"
|
|
796
1011
|
? estimateGeminiTtsCostUsd(readingText.length) : estimateFalCostUsd(readingText);
|
|
@@ -837,7 +1052,7 @@ async function runGenerate(options, io) {
|
|
|
837
1052
|
if (options.json) printCompactJson({ version: 1, status: "ok", id, path: relativePath,
|
|
838
1053
|
duration_s: duration, engine: options.engine, voice: provenance.voice, cost_usd: costUsd,
|
|
839
1054
|
applied: options.apply, caption_ref: options.captionRef, provenance, warnings,
|
|
840
|
-
...(options.speed !== null ? { speed_applied: options.engine
|
|
1055
|
+
...(options.speed !== null ? { speed_applied: ["voicevox", "irodori"].includes(options.engine) } : {}) }, io.log);
|
|
841
1056
|
else printJson(entry, io.log);
|
|
842
1057
|
return 0;
|
|
843
1058
|
}
|
|
@@ -856,6 +1071,16 @@ export async function runNarrationCommand(args, commandOptions = {}) {
|
|
|
856
1071
|
io.log(usage);
|
|
857
1072
|
return { exitCode: 0 };
|
|
858
1073
|
}
|
|
1074
|
+
if (args[0] === 'verify') {
|
|
1075
|
+
try { return { exitCode: await runVerify(parseVerifyArguments(args), io, commandOptions.verifyRuntime) }; }
|
|
1076
|
+
catch (error) {
|
|
1077
|
+
const code = error instanceof PublicError ? error.exitCode : 1;
|
|
1078
|
+
const message = error instanceof PublicError ? error.message : `聞き取りに失敗しました: ${error?.message ?? error}`;
|
|
1079
|
+
io.logError(message);
|
|
1080
|
+
printCompactJson({ error: message }, io.log);
|
|
1081
|
+
return { exitCode: code };
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
859
1084
|
if (["start", "stop"].includes(args[0])) {
|
|
860
1085
|
try {
|
|
861
1086
|
if (args.length !== 4 || args[1] !== "--engine" || args[2] !== "voicevox" || args[3] !== "--json") {
|
|
@@ -871,8 +1096,9 @@ export async function runNarrationCommand(args, commandOptions = {}) {
|
|
|
871
1096
|
}
|
|
872
1097
|
if (["engines", "voices"].includes(args[0])) {
|
|
873
1098
|
try {
|
|
874
|
-
const engine = parseListArguments(args);
|
|
875
|
-
printCompactJson(args[0] === "engines" ? await listEngines(commandOptions.engineRuntime
|
|
1099
|
+
const { engine, irodoriUrl } = parseListArguments(args);
|
|
1100
|
+
printCompactJson(args[0] === "engines" ? await listEngines({ ...commandOptions.engineRuntime, irodoriUrl }) :
|
|
1101
|
+
{ version: 1, engine, voices: await listVoices(engine) }, io.log);
|
|
876
1102
|
return { exitCode: 0 };
|
|
877
1103
|
} catch (error) {
|
|
878
1104
|
const message = error instanceof PublicError ? error.message : "声一覧を取得できませんでした";
|
package/src/store-command.mjs
CHANGED
|
@@ -412,17 +412,19 @@ export async function runStoreCommand(args, options = {}) {
|
|
|
412
412
|
if (dl.exitCode !== 0) return { exitCode: 1 };
|
|
413
413
|
const extractDir = path.join(stage, 'x');
|
|
414
414
|
mkdirSync(extractDir, { recursive: true });
|
|
415
|
-
//
|
|
415
|
+
// 共有 zip 展開器は tar → unzip → Expand-Archive の順で試す。
|
|
416
416
|
const extracted = options.extract
|
|
417
417
|
? options.extract(dl.filePath, extractDir)
|
|
418
|
-
:
|
|
419
|
-
const cmdArgs = tool === 'unzip' ? ['-o', '-q', dl.filePath, '-d', extractDir] : ['-xf', dl.filePath, '-C', extractDir];
|
|
418
|
+
: await (async () => {
|
|
420
419
|
try {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
420
|
+
const checkoutModule = new URL('../../asset-resolver/src/paid-zip.mjs', import.meta.url);
|
|
421
|
+
const vendorModule = new URL('../vendor/packages/asset-resolver/src/paid-zip.mjs', import.meta.url);
|
|
422
|
+
const moduleUrl = existsSync(checkoutModule) ? checkoutModule : vendorModule;
|
|
423
|
+
const { extractZipWithTools } = await import(moduleUrl.href);
|
|
424
|
+
extractZipWithTools(dl.filePath, extractDir);
|
|
425
|
+
return true;
|
|
426
|
+
} catch (error) { log(error.message); return false; }
|
|
427
|
+
})();
|
|
426
428
|
if (!extracted) {
|
|
427
429
|
log(`zip の展開に失敗しました。手動で展開してください: ${dl.filePath}`);
|
|
428
430
|
return { exitCode: 1 };
|
|
@@ -508,8 +510,8 @@ export async function runStoreCommand(args, options = {}) {
|
|
|
508
510
|
}
|
|
509
511
|
}
|
|
510
512
|
if (!runtimeIds) {
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
+
// 通常の launcher 配布には overlay-runtime が入る。旧配布物や破損した
|
|
514
|
+
// 配置で照合不能なら warning に落とし、要求 id を既知扱いして続行する。
|
|
513
515
|
requirementWarnings.push('runtime registry が見つからないため runtime id の照合をスキップしました。');
|
|
514
516
|
runtimeIds = manifest.requires?.runtimes ?? [];
|
|
515
517
|
}
|
package/src/vendor-sources.mjs
CHANGED
|
@@ -14,6 +14,20 @@ export const VENDOR_SOURCES = [
|
|
|
14
14
|
'presets/textanim',
|
|
15
15
|
'assets/font/noto-sans-jp/NotoSansJP-Variable.ttf',
|
|
16
16
|
'packages/schemas',
|
|
17
|
+
// schemas の asset / kit 検証は runtimes.mjs を実行し、runtime 宣言は src の
|
|
18
|
+
// レジストリ・ブラウザスクリプトと既定フォントをファイルとして読み込む。
|
|
19
|
+
'packages/overlay-runtime/runtimes.mjs',
|
|
20
|
+
'packages/overlay-runtime/src',
|
|
21
|
+
'packages/overlay-runtime/test-harness/fonts/ZenKakuGothicNew-Black.ttf',
|
|
22
|
+
// edit-lint と world-map validator が参照する実行時閉包。
|
|
23
|
+
'packages/render-cut/src/fragment-assets.mjs',
|
|
24
|
+
'packages/render-cut/src/render-inputs.mjs',
|
|
25
|
+
'packages/render-cut/src/caption-font.mjs',
|
|
26
|
+
'packages/render-cut/src/html-scan.mjs',
|
|
27
|
+
'packages/render-cut/src/library-reference.mjs',
|
|
28
|
+
'packages/word-book/src/index.mjs',
|
|
29
|
+
'packages/akari-tools/src/world/invariants.mjs',
|
|
30
|
+
'packages/akari-tools/src/world/normalize.mjs',
|
|
17
31
|
'packages/project-scaffold',
|
|
18
32
|
// analysis-report は package.json / README.md を capability source として既に収集する。
|
|
19
33
|
// 実行に必要な CLI と同居必須テンプレートだけを追加し、test/ は配布しない。
|
|
@@ -146,6 +146,8 @@
|
|
|
146
146
|
"skills/beat-sync-edit/SKILL.md",
|
|
147
147
|
"skills/beat-sync-edit/verify-loop.md",
|
|
148
148
|
"skills/compile-review-session/compilation-rules.md",
|
|
149
|
+
"skills/compile-review-session/dev-fixtures/issue-69-review-json/README.md",
|
|
150
|
+
"skills/compile-review-session/dev-fixtures/issue-79/README.md",
|
|
149
151
|
"skills/compile-review-session/dev-fixtures/README.md",
|
|
150
152
|
"skills/compile-review-session/SKILL.md",
|
|
151
153
|
"skills/create-project/SKILL.md",
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
> 正本の写し(2026-09-13・2026-09-21 改訂)・技術部分のみ。内部正本の §1〜§9 と §11 を公開向けに収録する。
|
|
4
4
|
> 節番号の対応: 本書 §1〜§9 = 内部正本 §1〜§9、本書 §10(コマ保存)= 内部正本 §11。
|
|
5
5
|
|
|
6
|
+
### 0-5. 裁定 D(確定・2026-09-23)
|
|
7
|
+
|
|
8
|
+
| # | 決定 | 理由 |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| D-1 | **音の空の枠** = 枠の長さの無音 wav を素材に持つ音声の media item。隣の meta.json は `kind: "audio"`・`status: "planned"`。置き場は映像の空の枠と同じ `assets/generated/` | ナレーションの差し替え先にできる |
|
|
11
|
+
| D-2 | **音声トラックの表示名は上から数える**(映像に近い方が A1、下へ A2, A3)。映像は下から(V1 がいちばん下の映像)。edit.json の `tracks[]` の並び・id は変えない | A1 の下に足したトラックが A2 になり、A1 の名前を保てる |
|
|
12
|
+
|
|
6
13
|
## 1. 継ぎ目(仮枠とタイムライン)
|
|
7
14
|
|
|
8
15
|
1. 仮枠は **edit.json v2 の media item** で、`sources[].path` が静止画(png / jpg)か文字カード png を指す。cuts で尺を持つ(静止画クリップ契約 2026-08-12 のとおり)
|
|
@@ -13,6 +20,7 @@
|
|
|
13
20
|
6. plan.json の仮枠役(`confidence` / `fill`)は退役。`plan-comments.json` の `pass: "scaffold"` の対象は `slot` から **clip id** へ
|
|
14
21
|
7. **空の枠**(2026-09-21): 仮枠ツール(F)で空いているところに描いた枠は、prompt 未記入の**文字カード png** を素材に持つ media item(絵のないクリップを文字カード png で表す規則の延長)。0.5 秒刻み・端に吸着・隣に食い込まない・0.5 秒未満にしない。専用トラックは作らない(どのトラックにも置ける)
|
|
15
22
|
実装: 選択 V / 分割 C / 仮枠 F(従来の A / B も有効)。仮枠はスナップ OFF でも近い可視端・再生ヘッドを 0.5 秒格子より優先し、`assets/generated/frame-<時刻>-<一意接尾辞>.png` と隣の `.meta.json` に保存する。
|
|
23
|
+
音声トラックにも無音 wav の枠を描ける。トラックの無い場所では上に映像トラック、下に音声トラックを足して置き、undo 1 回で追加したトラック・枠・source を消す。
|
|
16
24
|
8. **すき間から作る枠**(2026-09-21): すき間をクリック →「あいだを生成」で、すき間の位置と長さの枠を置く。前のクリップの最後のコマ・次のクリップの最初のコマを抽出して両端(②③)に入れる。動画からの抽出は 1 コマを `assets/captures/` へ書き出す(§10)
|
|
17
25
|
実装: 同じ映像トラックで前後にクリップがある 0.5 秒以上の空きだけをすき間とする。前は `source.out − 1/output.fps`(in 以上)、次は `source.in` の合成前のコマを使い、静止画は元パスを使う。
|
|
18
26
|
抽出 PNG は `assets/captures/frame-<素材名>-<秒>-<素材・更新時刻等のハッシュ>.png` に保存し、同じ素材・同じ時刻は再利用する。
|
|
@@ -47,7 +55,7 @@ meta の無い静止画は、最初に下書きを書くときにシェルが `k
|
|
|
47
55
|
```json
|
|
48
56
|
{
|
|
49
57
|
"version": 1,
|
|
50
|
-
"kind": "video", // "still" | "video" | "frames"
|
|
58
|
+
"kind": "video", // "still" | "video" | "frames" | "audio"(音の空の枠)
|
|
51
59
|
"status": "done", // "planned" | "generating" | "done" | "failed"
|
|
52
60
|
"model": { "id": "fal:h3-i2v", "endpoint": "minimax/h3/image-to-video", "as_of": "2026-09-12" },
|
|
53
61
|
"inputs": { // §2 の 9 スロット。値の無いスロットは null / []
|
|
@@ -207,6 +215,8 @@ Kling v3 standard i2v / Kling v3 pro i2v / Veo 3.1 first-last / Veo 3.1 referenc
|
|
|
207
215
|
|
|
208
216
|
## 7. 差し替え規則(done になったとき)
|
|
209
217
|
|
|
218
|
+
空の枠・静止画は AI タブの「静止画」(Codex)で作った絵へ差し替えられる。item と映像・色の設定を保ち、差し替えは undo 1 回で戻る。
|
|
219
|
+
|
|
210
220
|
### 7-1. 何を書き換えるか
|
|
211
221
|
|
|
212
222
|
同じ item の `sources[].path` を mp4 に。item id・トラック・`at` は不変。静止画の path は生成物 meta の `placeholder`(§3-2)に残る(由来。9/13 時点の meta は `inputs.first_frame`)。**1 手の undo**。
|
|
@@ -226,6 +236,8 @@ Kling v3 standard i2v / Kling v3 pro i2v / Veo 3.1 first-last / Veo 3.1 referenc
|
|
|
226
236
|
|
|
227
237
|
生成クリップの音声トラックは**既定 mute**(`item.mute: true`)。ナレーションと BGM が正。右パネルで解除できる。H3 のように欄なしで音が付くモデルでも同じ。
|
|
228
238
|
|
|
239
|
+
音の空の枠は AI タブのナレーションで声の素材へ差し替える。声が枠より長く、同じトラックの後ろに重なる場合は下の空いた音声トラックへ置き、既存のクリップは動かさない。
|
|
240
|
+
|
|
229
241
|
## 8. 費用承認(有償生成のゲート)
|
|
230
242
|
|
|
231
243
|
1. 語彙: 有償生成の承認は「**費用承認**」。「判子」は 9/6 契約どおり書き出しの 1 回にだけ使う
|
|
@@ -236,6 +248,8 @@ Kling v3 standard i2v / Kling v3 pro i2v / Veo 3.1 first-last / Veo 3.1 referenc
|
|
|
236
248
|
## 9. 用語(混同を防ぐ)
|
|
237
249
|
|
|
238
250
|
右パネルのタブ表示名は「AI」(id は `generation`)。入口は行為のタイル一覧で、選ぶと既存の生成フォームを専用パネルに表示する。
|
|
251
|
+
AI タブの「直す」に文字起こしを置き、台本パネルの処理を呼ぶ。済みなら字幕を見せる。
|
|
252
|
+
素材の AI タブは素材パネルで選ぶと右パネルに表示し、結果は新しい素材として増やす(文字起こしは台本)。
|
|
239
253
|
|
|
240
254
|
| 語 | 意味 |
|
|
241
255
|
|---|---|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.81",
|
|
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. Use `akari doctor --json` and run the path reported in `render_cut.path`. Full installations provide it in a monorepo checkout, ~/.akari/app, /Applications/AKARI Video.app/Contents/Resources/packages, or %LOCALAPPDATA%\\Programs\\@akari-videoshell\\resources\\packages.]",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|