akari-video 0.1.81 → 0.1.82
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/bin/akari.mjs +2 -0
- package/package.json +1 -1
- package/src/accept-command.mjs +10 -3
- package/src/doctor-command.mjs +1 -0
- package/src/narration-command.mjs +58 -79
- package/src/runtime-diagnostics.mjs +16 -19
- package/src/status-core/integrity.mjs +20 -4
- package/src/voice-command.mjs +441 -0
- package/vendor/.akari-capability-sources.json +2 -0
- package/vendor/docs/contract-2026-07-13-asset-library.md +1 -0
- package/vendor/docs/contract-2026-08-02-creator-root-v1.md +3 -3
- package/vendor/docs/contract-2026-08-03-caption-display-encoding-qc-v1.md +22 -20
- package/vendor/docs/contract-2026-08-12-chat-approval-v0.md +1 -1
- package/vendor/docs/contract-2026-09-13-generation-v0.md +1 -0
- package/vendor/docs/contract-2026-09-24-style-v0.md +69 -0
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/akari-vibe/README.md +1 -1
- package/vendor/packages/creator-root/src/credentials.mjs +119 -0
- package/vendor/packages/creator-root/src/index.mjs +1 -0
- package/vendor/packages/creator-root/test/credentials.test.mjs +75 -0
- package/vendor/packages/edit-lint/src/edit-lint.mjs +6 -3
- package/vendor/packages/edit-store/lib/audio-schedule.d.ts +1 -0
- package/vendor/packages/edit-store/lib/audio-schedule.js +19 -18
- package/vendor/packages/edit-store/lib/caption-display.js +17 -5
- package/vendor/packages/edit-store/lib/caption-store.d.ts +3 -0
- package/vendor/packages/edit-store/lib/caption-store.js +15 -4
- package/vendor/packages/edit-store/lib/edit-v2-item-write.d.ts +3 -1
- package/vendor/packages/edit-store/lib/edit-v2-item-write.js +19 -4
- package/vendor/packages/edit-store/lib/index.d.ts +1 -0
- package/vendor/packages/edit-store/lib/index.js +1 -0
- package/vendor/packages/edit-store/lib/internal-model.d.ts +1 -0
- package/vendor/packages/edit-store/lib/internal-model.js +4 -3
- package/vendor/packages/edit-store/lib/legacy-audio-view.d.ts +3 -1
- package/vendor/packages/edit-store/lib/legacy-audio-view.js +10 -4
- package/vendor/packages/edit-store/lib/migrate/legacy-parse.d.ts +1 -0
- package/vendor/packages/edit-store/lib/migrate/legacy-parse.js +1 -0
- package/vendor/packages/edit-store/lib/transform-keyframe-edit.d.ts +9 -0
- package/vendor/packages/edit-store/lib/transform-keyframe-edit.js +222 -0
- package/vendor/packages/edit-store/lib/webview-kernel.js +33 -25
- package/vendor/packages/overlay-runtime/src/interaction.css +11 -0
- package/vendor/packages/overlay-runtime/src/interaction.js +138 -17
- package/vendor/packages/overlay-runtime/src/selection-scope.mjs +9 -1
- package/vendor/packages/schemas/bin/validate-captions.mjs +4 -1
- package/vendor/packages/schemas/captions.schema.json +1 -0
- package/vendor/packages/schemas/test/validate-captions.test.mjs +9 -0
- package/vendor/skills/analyze-footage/bin/person-matte/resolve-packages.mjs +97 -21
- package/vendor/skills/analyze-footage/bin/transcribe-cloud.mjs +27 -38
- package/vendor/skills/analyze-footage/test/entrypoint-symlink.test.mjs +26 -1
- package/vendor/skills/analyze-footage/test/resolve-packages-newest.test.mjs +129 -0
- package/vendor/skills/generate-narration/SKILL.md +1 -1
- package/vendor/skills/generate-narration/engines.md +11 -2
- package/vendor/skills/generate-narration/voice-profile-setup.md +39 -15
- package/vendor/skills/manage-connections/SKILL.md +3 -3
- package/vendor/skills/manage-connections/bin/doctor.mjs +21 -44
- package/vendor/skills/manage-connections/bin/resolve-packages.mjs +97 -21
- package/vendor/skills/manage-connections/bin/test/resolve-packages.test.mjs +31 -0
- package/vendor/skills/manage-connections/dev-fixtures/resolve-packages-newest/README.md +17 -0
- package/vendor/skills/manage-connections/dev-fixtures/resolve-packages-newest/reproduce.mjs +97 -0
- package/vendor/skills/manage-connections/test/resolve-packages-newest.test.mjs +129 -0
- package/vendor/skills/setup-audio-library/drop-folder.md +3 -3
- package/vendor/skills/setup-chat-approval/SKILL.md +1 -1
- package/vendor/skills/setup-chat-approval/bin/doctor.mjs +15 -27
- package/vendor/skills/setup-chat-approval/bin/find-chat-id.mjs +10 -10
- package/vendor/skills/setup-chat-approval/bin/resolve-packages.mjs +97 -21
- package/vendor/skills/setup-chat-approval/bin/test/distribution.test.mjs +33 -0
- package/vendor/skills/setup-chat-approval/setup-token.md +1 -1
- package/vendor/skills/setup-chat-approval/test/resolve-packages-newest.test.mjs +129 -0
package/bin/akari.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { run, runUpdateCommand } from '../src/cli.mjs';
|
|
|
3
3
|
import { runInitCommand } from '../src/init-command.mjs';
|
|
4
4
|
import { runNewCommand } from '../src/new-command.mjs';
|
|
5
5
|
import { runNarrationCommand } from '../src/narration-command.mjs';
|
|
6
|
+
import { runVoiceCommand } from '../src/voice-command.mjs';
|
|
6
7
|
import { runInternalCommand } from '../src/internal-command.mjs';
|
|
7
8
|
import { runSoundsCommand } from '../src/sounds-setup.mjs';
|
|
8
9
|
import { runStatusCommand } from '../src/status-command.mjs';
|
|
@@ -78,6 +79,7 @@ const invoke = (argv[0] === '--version' || argv[0] === '-v') ? printVersion()
|
|
|
78
79
|
: argv[0] === 'new' ? runNewCommand(argv.slice(1))
|
|
79
80
|
: argv[0] === 'skills' ? runSkillsCommand(argv.slice(1))
|
|
80
81
|
: argv[0] === 'narration' ? runNarrationCommand(argv.slice(1))
|
|
82
|
+
: argv[0] === 'voice' ? runVoiceCommand(argv.slice(1))
|
|
81
83
|
: argv[0] === 'internal' ? runInternalCommand(argv.slice(1))
|
|
82
84
|
: argv[0] === 'sounds' ? runSoundsCommand(argv.slice(1))
|
|
83
85
|
: argv[0] === 'status' ? runStatusCommand(argv.slice(1))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.82",
|
|
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/accept-command.mjs
CHANGED
|
@@ -42,9 +42,8 @@ export async function runAcceptCommand(argv, options = {}) {
|
|
|
42
42
|
const candidate = before.candidate;
|
|
43
43
|
const phrase = `ACCEPT ${candidate.artifact_sha256}`;
|
|
44
44
|
log(`Artifact: ${candidate.artifact}\nArtifact SHA-256: ${candidate.artifact_sha256}\nReceipt: ${candidate.receipt}\nReceipt SHA-256: ${candidate.receipt_sha256}\n`);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
45
|
+
const audioQcMessage = formatAudioQcAcceptance(candidate.audio_qc);
|
|
46
|
+
if (audioQcMessage) log(audioQcMessage);
|
|
48
47
|
|
|
49
48
|
const prompt = options.prompt ?? promptFromTTY;
|
|
50
49
|
let actorId;
|
|
@@ -88,6 +87,14 @@ export async function runAcceptCommand(argv, options = {}) {
|
|
|
88
87
|
}
|
|
89
88
|
}
|
|
90
89
|
|
|
90
|
+
export function formatAudioQcAcceptance(audioQc) {
|
|
91
|
+
if (audioQc?.verdict === "PASS") return "Audio QC: PASS.\n";
|
|
92
|
+
if (audioQc?.verdict === "INCONCLUSIVE") {
|
|
93
|
+
return `WARNING: audio_qc is INCONCLUSIVE; this is not an audio conformance PASS.\nConfigured: ${JSON.stringify(audioQc.configured)}\nFilter report: ${JSON.stringify(audioQc.filter_report)}\nDecoded artifact measurement: ${JSON.stringify(audioQc.decoded_measurement)}\n`;
|
|
94
|
+
}
|
|
95
|
+
return "";
|
|
96
|
+
}
|
|
97
|
+
|
|
91
98
|
export function parseAcceptArguments(argv, cwd = process.cwd()) {
|
|
92
99
|
let projectRoot = cwd;
|
|
93
100
|
let pathSeen = false;
|
package/src/doctor-command.mjs
CHANGED
|
@@ -41,6 +41,7 @@ export function formatDoctorReport(report) {
|
|
|
41
41
|
: report.fal_key?.source === 'credentials.env'
|
|
42
42
|
? report.fal_key.credentials_path
|
|
43
43
|
: '環境変数にも credentials.env にもありません'],
|
|
44
|
+
['鍵の置き場', report.fal_key?.location ?? 'なし', '新 / 旧 / 両方'],
|
|
44
45
|
['path', report.path.on_path ? 'ok' : 'missing', report.path.cli_shim_dir],
|
|
45
46
|
];
|
|
46
47
|
const widths = [
|
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { spawn, spawnSync } from "node:child_process";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
9
|
import { resolveLauncherAssets } from "./repo-assets.mjs";
|
|
10
|
+
import { listProfiles, readFalKey, resolveVoiceProfile } from "./voice-command.mjs";
|
|
10
11
|
|
|
11
12
|
const VOICEVOX_BASE_URL = "http://127.0.0.1:50021";
|
|
12
13
|
const VOICEVOX_RUN_ENV = "VOICEVOX_RUN";
|
|
@@ -206,6 +207,14 @@ async function runVerify(options, io, runtime = {}) {
|
|
|
206
207
|
} finally { fs.rmSync(temporary, { recursive: true, force: true }); }
|
|
207
208
|
}
|
|
208
209
|
|
|
210
|
+
// 録音の原稿照合を、narration verify と同じ backend・正規化・一致率で再利用する。
|
|
211
|
+
export async function verifyNarrationAudio(audio, expected, backend = "auto", runtime = {}) {
|
|
212
|
+
const lines = [];
|
|
213
|
+
const code = await runVerify({ project: path.dirname(audio), audio, text: expected, backend,
|
|
214
|
+
reading: null, record: null, checkBackend: false }, { log: line => lines.push(line), logError: () => {} }, runtime);
|
|
215
|
+
return { code, result: JSON.parse(lines.at(-1)) };
|
|
216
|
+
}
|
|
217
|
+
|
|
209
218
|
class PublicError extends Error {
|
|
210
219
|
constructor(message, exitCode = 1) {
|
|
211
220
|
super(message);
|
|
@@ -301,7 +310,7 @@ function parseArguments(argv) {
|
|
|
301
310
|
}
|
|
302
311
|
if (options.engine === "irodori") {
|
|
303
312
|
options.voice ??= "narrator-male";
|
|
304
|
-
if (!IRODORI_RECIPES.some(recipe => recipe.id === options.voice) && options.voice !== "custom") throw new PublicError("彩の声レシピが不明です", 2);
|
|
313
|
+
if (!options.profile && !IRODORI_RECIPES.some(recipe => recipe.id === options.voice) && options.voice !== "custom") throw new PublicError("彩の声レシピが不明です", 2);
|
|
305
314
|
if (options.voice === "custom" && !options.style?.trim()) throw new PublicError("自分で書く声には --style が必要です", 2);
|
|
306
315
|
options.irodori = irodoriEndpoint(options.irodoriUrl);
|
|
307
316
|
irodoriTimeout();
|
|
@@ -376,80 +385,34 @@ function relativeOutputPath(id, engine) {
|
|
|
376
385
|
return `out/narration/${id}.${extensionFor(engine)}`;
|
|
377
386
|
}
|
|
378
387
|
|
|
379
|
-
// --- credentials.env(fal-qwen3 用。skills/analyze-footage/bin/transcribe-cloud.mjs と同型) ---
|
|
380
|
-
|
|
381
|
-
function credentialsPath() {
|
|
382
|
-
return path.resolve(
|
|
383
|
-
process.env.AKARI_CREDENTIALS_FILE
|
|
384
|
-
?? path.join(os.homedir(), ".config", "akari-video", "credentials.env"),
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function readCredentials() {
|
|
389
|
-
let source;
|
|
390
|
-
try {
|
|
391
|
-
source = fs.readFileSync(credentialsPath(), "utf8");
|
|
392
|
-
} catch (error) {
|
|
393
|
-
if (error?.code === "ENOENT") return new Map();
|
|
394
|
-
throw new PublicError("credentials.env を読めません");
|
|
395
|
-
}
|
|
396
|
-
const values = new Map();
|
|
397
|
-
for (const originalLine of source.split(/\r?\n/)) {
|
|
398
|
-
const line = originalLine.trim();
|
|
399
|
-
if (!line || line.startsWith("#")) continue;
|
|
400
|
-
const separator = line.indexOf("=");
|
|
401
|
-
if (separator < 1) continue;
|
|
402
|
-
const name = line.slice(0, separator).trim();
|
|
403
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue;
|
|
404
|
-
let value = line.slice(separator + 1).trim();
|
|
405
|
-
if (
|
|
406
|
-
value.length >= 2
|
|
407
|
-
&& ((value.startsWith('"') && value.endsWith('"'))
|
|
408
|
-
|| (value.startsWith("'") && value.endsWith("'")))
|
|
409
|
-
) {
|
|
410
|
-
value = value.slice(1, -1);
|
|
411
|
-
}
|
|
412
|
-
values.set(name, value);
|
|
413
|
-
}
|
|
414
|
-
return values;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
388
|
function resolveFalKey() {
|
|
418
|
-
const secret =
|
|
389
|
+
const secret = readFalKey(process.env);
|
|
419
390
|
if (!secret) {
|
|
420
391
|
throw new PublicError(
|
|
421
|
-
`FAL_KEY
|
|
392
|
+
`FAL_KEY が未設定です。<AKARI_HOME>/credentials.env に FAL_KEY=... を 1 行追加してください`
|
|
422
393
|
+ "(取得先: https://fal.ai/dashboard/keys)。",
|
|
423
394
|
);
|
|
424
395
|
}
|
|
425
396
|
return secret;
|
|
426
397
|
}
|
|
427
398
|
|
|
428
|
-
function
|
|
429
|
-
|
|
399
|
+
function readProfileMeta(profileName) {
|
|
400
|
+
let meta;
|
|
401
|
+
try { meta = resolveVoiceProfile(profileName).meta; }
|
|
402
|
+
catch { throw new PublicError(`声プロファイルが見つかりません: ${profileName}`, 2); }
|
|
403
|
+
const embedding_source_url = meta.engines?.["fal-qwen3"]?.embedding_source_url;
|
|
404
|
+
if (!embedding_source_url) throw new PublicError(`この声には fal の写しがありません。akari voice copy で作ってください`, 2);
|
|
405
|
+
if (!meta.reference_text) throw new PublicError(`声プロファイルの reference_text がありません: ${profileName}`, 2);
|
|
406
|
+
return { ...meta, embedding_source_url };
|
|
430
407
|
}
|
|
431
408
|
|
|
432
|
-
function
|
|
433
|
-
const metaPath = profileMetaPath(profileName);
|
|
434
|
-
let raw;
|
|
435
|
-
try {
|
|
436
|
-
raw = fs.readFileSync(metaPath, "utf8");
|
|
437
|
-
} catch {
|
|
438
|
-
throw new PublicError(`声プロファイルが見つかりません: ${metaPath}`);
|
|
439
|
-
}
|
|
409
|
+
function irodoriProfileVoice(profileName) {
|
|
440
410
|
let meta;
|
|
441
|
-
try {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (typeof meta.embedding_source_url !== "string" || !meta.embedding_source_url) {
|
|
447
|
-
throw new PublicError(`声プロファイルの meta.json に embedding_source_url がありません: ${metaPath}`);
|
|
448
|
-
}
|
|
449
|
-
if (typeof meta.reference_text !== "string" || !meta.reference_text) {
|
|
450
|
-
throw new PublicError(`声プロファイルの meta.json に reference_text がありません: ${metaPath}`);
|
|
451
|
-
}
|
|
452
|
-
return meta;
|
|
411
|
+
try { meta = resolveVoiceProfile(profileName).meta; }
|
|
412
|
+
catch { throw new PublicError(`声プロファイルが見つかりません: ${profileName}`, 2); }
|
|
413
|
+
const voiceId = meta.engines?.irodori?.voice_id;
|
|
414
|
+
if (!voiceId) throw new PublicError("この声には彩の写しがありません。akari voice copy で作ってください", 2);
|
|
415
|
+
return voiceId;
|
|
453
416
|
}
|
|
454
417
|
|
|
455
418
|
function buildFalPayload(readingText, meta) {
|
|
@@ -786,13 +749,14 @@ async function probeIrodori(endpoint, fetchImpl = fetch) {
|
|
|
786
749
|
}
|
|
787
750
|
|
|
788
751
|
async function synthesizeIrodori(readingText, options, fetchImpl = fetch) {
|
|
789
|
-
const caption = options.style?.trim() || IRODORI_RECIPES.find(recipe => recipe.id === options.voice)?.caption;
|
|
752
|
+
const caption = options.profile ? null : options.style?.trim() || IRODORI_RECIPES.find(recipe => recipe.id === options.voice)?.caption;
|
|
753
|
+
const profileVoice = options.profile ? irodoriProfileVoice(options.profile) : null;
|
|
790
754
|
let response;
|
|
791
755
|
try {
|
|
792
756
|
response = await fetchImpl(`${options.irodori.base}/v1/audio/speech`, {
|
|
793
757
|
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 } }),
|
|
758
|
+
body: JSON.stringify({ model: "irodori-tts", input: readingText, voice: profileVoice ?? "none", response_format: "wav",
|
|
759
|
+
speed: options.speed ?? 1, ...(caption ? { irodori: { caption } } : {}) }),
|
|
796
760
|
});
|
|
797
761
|
} catch (error) { throw new PublicError(`彩サーバーに接続できません: ${error?.message ?? error}`); }
|
|
798
762
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
@@ -802,6 +766,21 @@ async function synthesizeIrodori(readingText, options, fetchImpl = fetch) {
|
|
|
802
766
|
return buffer;
|
|
803
767
|
}
|
|
804
768
|
|
|
769
|
+
function resolveProfileEnv(runtime = {}) {
|
|
770
|
+
const env = runtime.env || process.env;
|
|
771
|
+
const homeDir = env.HOME || env.USERPROFILE || os.homedir();
|
|
772
|
+
return { ...env, HOME: homeDir, AKARI_HOME: env.AKARI_HOME || path.join(homeDir, ".akari") };
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function listFalProfiles(runtime = {}) {
|
|
776
|
+
try {
|
|
777
|
+
return listProfiles(resolveProfileEnv(runtime)).filter((profile) => profile.legacy || profile.engines.includes("fal-qwen3"));
|
|
778
|
+
} catch {
|
|
779
|
+
// listProfiles はメタデータの読み取り失敗時に throw する。CLI の一覧は維持する。
|
|
780
|
+
return [];
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
805
784
|
async function listEngines(runtime = {}) {
|
|
806
785
|
let voicevox;
|
|
807
786
|
const state = await probeVoicevox(runtime.fetchImpl || fetch);
|
|
@@ -819,12 +798,13 @@ async function listEngines(runtime = {}) {
|
|
|
819
798
|
? { state: "needs", label: "VOICEVOX を起動します(自動)", detail }
|
|
820
799
|
: { state: "unconfigured", label: "VOICEVOX をインストール", detail: { ...detail, setup_url: "https://voicevox.hiroshiba.jp/" } };
|
|
821
800
|
}
|
|
822
|
-
|
|
801
|
+
let configured = false;
|
|
802
|
+
try { configured = Boolean(readFalKey(runtime.env || process.env)); }
|
|
803
|
+
catch { /* 配布物の creator-root や鍵ファイルを読めなくても一覧は返す。 */ }
|
|
823
804
|
const falAvailability = configured
|
|
824
805
|
? { state: "available", label: "fal を使用できます" }
|
|
825
806
|
: { state: "unconfigured", label: "fal の鍵を登録" };
|
|
826
|
-
const
|
|
827
|
-
const hasProfiles = fs.existsSync(profilesDir) && fs.readdirSync(profilesDir, { withFileTypes: true }).some((entry) => entry.isDirectory());
|
|
807
|
+
const profilesWithFal = listFalProfiles(runtime).length;
|
|
828
808
|
let endpoint;
|
|
829
809
|
try { endpoint = irodoriEndpoint(runtime.irodoriUrl, runtime.env || process.env); }
|
|
830
810
|
catch (error) { if (!(error instanceof PublicError)) throw error; }
|
|
@@ -839,11 +819,11 @@ async function listEngines(runtime = {}) {
|
|
|
839
819
|
: irodoriAvailable
|
|
840
820
|
? { state: "available", label: "お試し · 接続済み", detail: { url: endpoint.server } }
|
|
841
821
|
: { state: "unconfigured", label: "Irodori サーバーにつながりません(お試し)", detail: { setup_url: IRODORI_SETUP_URL } } },
|
|
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 :
|
|
822
|
+
{ 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, detail: { profiles_with_fal: profilesWithFal } } : profilesWithFal ? { state: "available", label: "声プロファイルを使用できます", detail: { profiles_with_fal: profilesWithFal } } : { state: "needs", label: "fal の写しがある声がありません(自分の声をつくる)", detail: { profiles_with_fal: 0 } }, credit_required: false, supports: { speed: false, style: false } },
|
|
843
823
|
] };
|
|
844
824
|
}
|
|
845
825
|
|
|
846
|
-
async function listVoices(engine) {
|
|
826
|
+
async function listVoices(engine, runtime = {}) {
|
|
847
827
|
if (engine === "irodori") return [...IRODORI_RECIPES.map(({ id, label, default: isDefault }) => ({ id, label, ...(isDefault ? { default: true } : {}) })),
|
|
848
828
|
{ id: "custom", label: "自分で書く(声の指示)" }];
|
|
849
829
|
if (engine === "gemini-tts") return GEMINI_VOICES;
|
|
@@ -861,9 +841,7 @@ async function listVoices(engine) {
|
|
|
861
841
|
}
|
|
862
842
|
}
|
|
863
843
|
if (engine === "fal-qwen3") {
|
|
864
|
-
|
|
865
|
-
return fs.existsSync(profilesDir) ? fs.readdirSync(profilesDir, { withFileTypes: true })
|
|
866
|
-
.filter((entry) => entry.isDirectory()).map((entry) => ({ id: entry.name, label: entry.name })) : [];
|
|
844
|
+
return listFalProfiles(runtime).map(({ id, label, legacy }) => ({ id, label, ...(legacy ? { legacy: true } : {}) }));
|
|
867
845
|
}
|
|
868
846
|
throw new PublicError(`声一覧に対応していないエンジンです: ${engine}`, 2);
|
|
869
847
|
}
|
|
@@ -907,16 +885,17 @@ async function runDryRun(options, readingText, io) {
|
|
|
907
885
|
emit({
|
|
908
886
|
dry_run: true, engine: options.engine, output_path: outputPath,
|
|
909
887
|
estimated_cost_usd: estimateGeminiTtsCostUsd(readingText.length),
|
|
910
|
-
request: { endpoint: GEMINI_TTS_URL, headers: { Authorization: `Key ${maskKey(
|
|
888
|
+
request: { endpoint: GEMINI_TTS_URL, headers: { Authorization: `Key ${maskKey(readFalKey(process.env))}` },
|
|
911
889
|
body: { prompt: readingText, voice: options.voice, model: "gemini-2.5-flash-tts", output_format: "mp3", language_code: "Japanese (Japan)", ...(options.style ? { style_instructions: options.style } : {}) } },
|
|
912
890
|
}, io.log);
|
|
913
891
|
return;
|
|
914
892
|
}
|
|
915
893
|
if (options.engine === "irodori") {
|
|
894
|
+
const profileVoice = options.profile ? irodoriProfileVoice(options.profile) : null;
|
|
916
895
|
emit({ dry_run: true, engine: options.engine, output_path: outputPath, estimated_cost_usd: 0,
|
|
917
896
|
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);
|
|
897
|
+
voice: profileVoice ?? "none", response_format: "wav", speed: options.speed ?? 1,
|
|
898
|
+
...(profileVoice ? {} : { irodori: { caption: options.style?.trim() || IRODORI_RECIPES.find(recipe => recipe.id === options.voice)?.caption } }) } } }, io.log);
|
|
920
899
|
return;
|
|
921
900
|
}
|
|
922
901
|
const falKey = resolveFalKey();
|
|
@@ -1004,7 +983,7 @@ async function runGenerate(options, io) {
|
|
|
1004
983
|
}
|
|
1005
984
|
} else if (options.engine === "irodori") {
|
|
1006
985
|
audioBuffer = await synthesizeIrodori(readingText, options);
|
|
1007
|
-
provenance = { provider: "irodori", engine: "irodori-tts-v4-small", voice: options.style?.trim() ? "caption:custom" : `recipe:${options.voice}`,
|
|
986
|
+
provenance = { provider: "irodori", engine: "irodori-tts-v4-small", voice: options.profile ? `profile:${options.profile}` : options.style?.trim() ? "caption:custom" : `recipe:${options.voice}`,
|
|
1008
987
|
generated_at: new Date().toISOString(), experimental: true, server: options.irodori.server };
|
|
1009
988
|
} else {
|
|
1010
989
|
const estimatedCostUsd = options.engine === "gemini-tts"
|
|
@@ -1098,7 +1077,7 @@ export async function runNarrationCommand(args, commandOptions = {}) {
|
|
|
1098
1077
|
try {
|
|
1099
1078
|
const { engine, irodoriUrl } = parseListArguments(args);
|
|
1100
1079
|
printCompactJson(args[0] === "engines" ? await listEngines({ ...commandOptions.engineRuntime, irodoriUrl }) :
|
|
1101
|
-
{ version: 1, engine, voices: await listVoices(engine) }, io.log);
|
|
1080
|
+
{ version: 1, engine, voices: await listVoices(engine, commandOptions.engineRuntime) }, io.log);
|
|
1102
1081
|
return { exitCode: 0 };
|
|
1103
1082
|
} catch (error) {
|
|
1104
1083
|
const message = error instanceof PublicError ? error.message : "声一覧を取得できませんでした";
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import path, { dirname, join, resolve } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
5
|
|
|
6
6
|
import { findExecutable } from './path-lookup.mjs';
|
|
7
7
|
import { readInstalledAppVersionInfo, readOwnVersion, resolveAkariHome } from './update-check.mjs';
|
|
8
|
+
import { resolveLauncherAssets } from './repo-assets.mjs';
|
|
9
|
+
|
|
10
|
+
const creatorRootModulePath = resolveLauncherAssets().creatorRootModulePath;
|
|
11
|
+
const creatorCredentials = creatorRootModulePath ? await import(pathToFileURL(creatorRootModulePath).href) : null;
|
|
8
12
|
|
|
9
13
|
const THIS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
10
14
|
const ANCESTOR_SEARCH_MAX_DEPTH = 10;
|
|
@@ -162,26 +166,19 @@ export async function resolveDoctorReport(options = {}) {
|
|
|
162
166
|
}
|
|
163
167
|
|
|
164
168
|
function resolveFalKeySource({ env, homeDirectory }) {
|
|
165
|
-
const
|
|
166
|
-
|
|
169
|
+
const credentialEnv = homeDirectory ? { ...env, HOME: homeDirectory, USERPROFILE: homeDirectory } : env;
|
|
170
|
+
const credentialsPath = creatorCredentials?.credentialsPaths(credentialEnv).primary
|
|
171
|
+
?? join(resolveAkariHome(credentialEnv), 'credentials.env');
|
|
172
|
+
let state;
|
|
173
|
+
try { state = creatorCredentials?.readCredentials(credentialEnv); }
|
|
174
|
+
catch { state = { values: new Map(), primaryExists: false, legacyExists: false }; }
|
|
175
|
+
state ??= { values: new Map(), primaryExists: false, legacyExists: false };
|
|
176
|
+
const location = state.primaryExists && state.legacyExists ? '両方' : state.primaryExists ? '新' : state.legacyExists ? '旧' : 'なし';
|
|
167
177
|
if (typeof env.FAL_KEY === 'string' && env.FAL_KEY.trim().length > 0) {
|
|
168
|
-
return { source: 'env', credentials_path: credentialsPath };
|
|
169
|
-
}
|
|
170
|
-
try {
|
|
171
|
-
const source = readFileSync(credentialsPath, 'utf8');
|
|
172
|
-
for (const originalLine of source.split(/\r?\n/u)) {
|
|
173
|
-
const line = originalLine.trim();
|
|
174
|
-
if (!line || line.startsWith('#')) continue;
|
|
175
|
-
const separator = line.indexOf('=');
|
|
176
|
-
if (separator < 1 || line.slice(0, separator).trim() !== 'FAL_KEY') continue;
|
|
177
|
-
if (line.slice(separator + 1).trim().length > 0) {
|
|
178
|
-
return { source: 'credentials.env', credentials_path: credentialsPath };
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
} catch {
|
|
182
|
-
// credentials.env が無い・読めない場合も doctor 自体は継続する。
|
|
178
|
+
return { source: 'env', credentials_path: credentialsPath, location };
|
|
183
179
|
}
|
|
184
|
-
return { source: '
|
|
180
|
+
if (state.values.get('FAL_KEY')?.trim()) return { source: 'credentials.env', credentials_path: credentialsPath, location };
|
|
181
|
+
return { source: 'missing', credentials_path: credentialsPath, location };
|
|
185
182
|
}
|
|
186
183
|
|
|
187
184
|
export async function resolveGpuExportAvailability(options = {}) {
|
|
@@ -202,7 +202,7 @@ export function contentAddressedJsonFilenameMatches(filePath, sha256Value, basen
|
|
|
202
202
|
return basenameImpl(filePath) === `${sha256Value}.json`;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
function validateAudioQc(value, problems, warnings) {
|
|
205
|
+
export function validateAudioQc(value, problems, warnings) {
|
|
206
206
|
if (!isRecord(value) || !isRecord(value.configured)
|
|
207
207
|
|| !isFiniteNumber(value.configured.integrated_lufs) || !isFiniteNumber(value.configured.true_peak_dbtp)
|
|
208
208
|
|| !isNonEmptyString(value.tool_version)) {
|
|
@@ -226,7 +226,7 @@ function validateAudioQc(value, problems, warnings) {
|
|
|
226
226
|
}
|
|
227
227
|
return;
|
|
228
228
|
}
|
|
229
|
-
if (value.verdict !== "INCONCLUSIVE") {
|
|
229
|
+
if (value.verdict !== "INCONCLUSIVE" && value.verdict !== "PASS") {
|
|
230
230
|
problems.push("audio_qc verdict is invalid");
|
|
231
231
|
return;
|
|
232
232
|
}
|
|
@@ -236,10 +236,26 @@ function validateAudioQc(value, problems, warnings) {
|
|
|
236
236
|
} else {
|
|
237
237
|
validateQcReport(value.decoded_measurement, ["input_i", "input_tp"], "audio_qc.decoded_measurement", problems);
|
|
238
238
|
}
|
|
239
|
-
if (value.error !== undefined) problems.push("audio_qc
|
|
240
|
-
|
|
239
|
+
if (value.error !== undefined) problems.push("audio_qc successful measurement must not contain an error");
|
|
240
|
+
if (value.verdict === "PASS") {
|
|
241
|
+
const inputI = value.decoded_measurement?.normalized?.input_i;
|
|
242
|
+
const inputTp = value.decoded_measurement?.normalized?.input_tp;
|
|
243
|
+
if (!isNonEmptyString(value.tool_version)
|
|
244
|
+
|| !isFiniteNumber(inputI)
|
|
245
|
+
|| !isFiniteNumber(inputTp)
|
|
246
|
+
|| Math.abs(inputI - value.configured.integrated_lufs) > INTEGRATED_LOUDNESS_TOLERANCE_LU
|
|
247
|
+
|| inputTp > value.configured.true_peak_dbtp + TRUE_PEAK_EXCEEDED_TOLERANCE_DB) {
|
|
248
|
+
problems.push("audio_qc PASS does not match configured target and decoded measurement");
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
warnings.push("audio_qc is INCONCLUSIVE; configured target, filter report, and decoded measurement require human review");
|
|
252
|
+
}
|
|
241
253
|
}
|
|
242
254
|
|
|
255
|
+
// Keep these in parity with render-cut/src/audio-qc.mjs; status-core is a self-contained mirror.
|
|
256
|
+
const INTEGRATED_LOUDNESS_TOLERANCE_LU = 1.0;
|
|
257
|
+
const TRUE_PEAK_EXCEEDED_TOLERANCE_DB = 0.1;
|
|
258
|
+
|
|
243
259
|
// akari-audio-qc-decimal-v1: exact finite base-10 text only. This intentionally duplicates the
|
|
244
260
|
// render-cut grammar because status-core is distributed as a self-contained generated mirror.
|
|
245
261
|
const STRICT_FINITE_DECIMAL = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/u;
|