akari-video 0.1.53 → 0.1.54
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/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/akari-tools/README.md +2 -0
- package/vendor/packages/asset-resolver/test/project-references.test.mjs +4 -2
- package/vendor/packages/edit-store/lib/caption-line-diff.d.ts +72 -0
- package/vendor/packages/edit-store/lib/caption-line-diff.js +167 -0
- package/vendor/skills/manage-connections/bin/doctor.mjs +39 -38
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.54",
|
|
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": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.54",
|
|
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": [
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
- `akari media filmstrip <target>` — 素材全体の流れをコンタクトシートにする。
|
|
8
8
|
- `akari media waveform <target>` — 波形、無音区間、ラウドネスを調べる。
|
|
9
9
|
- `akari media transcribe <target>` — ローカル既定の時刻付き文字起こしを返す。
|
|
10
|
+
- `akari media transcribe-diff <target> [--engines a,b,c]` — 最新のエンジン別文字起こしを文字単位で比較し、`diff.json` と JSON 要約を返す。句読点だけの差は除外し、1 エンジンなら比較を省略する。
|
|
11
|
+
- `akari media transcribe-cuts <target> [--basis b] [--silence-min 1.5] [--silence-break 3.0] [--silence-keep 0.5]` — フィラー・言い直し・無音・未認識を `cuts.json` に候補として保存し、JSON 要約を返す。既存の採否を引き継ぎ、手直しした字幕に重なる候補は既定 OFF にする。
|
|
10
12
|
- `akari media audio-level <projectDir>` — `gain_db` 未指定の音声素材を測り、役割別の提案値を表にする。`--write` で `edit.json` へ数値を保存し、`--json` で JSON 配列を返す。
|
|
11
13
|
|
|
12
14
|
## 音声素材の挿入レベル
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
2
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import test from 'node:test';
|
|
@@ -57,11 +57,13 @@ test('resolveLibraryFallback accepts only a declared regular file inside the lib
|
|
|
57
57
|
await writeFile(file, 'frame', 'utf8');
|
|
58
58
|
const references = [{ id: 'card', category: 'still' }];
|
|
59
59
|
|
|
60
|
+
// resolveLibraryFallback は symlink 脱出を塞ぐため realpath を返す。macOS の os.tmpdir() は
|
|
61
|
+
// /var/folders/... (/private/var/... への symlink)なので、期待値も同じ正規化を通す。
|
|
60
62
|
assert.equal(resolveLibraryFallback({
|
|
61
63
|
declaredPath: 'assets/still/card/nested/frame.png',
|
|
62
64
|
references,
|
|
63
65
|
akariAssetsDir: assetsDir,
|
|
64
|
-
}), file);
|
|
66
|
+
}), realpathSync(file));
|
|
65
67
|
assert.equal(resolveLibraryFallback({
|
|
66
68
|
declaredPath: 'assets/still/card/../../outside.png',
|
|
67
69
|
references,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文字起こしパネル(1 行 = 1 字幕の Monaco 編集)で **行数が変わる編集** をしたときの
|
|
3
|
+
* 「行の差分 → 字幕操作列」への写像と、その操作に伴う時刻割り付け・id 採番。
|
|
4
|
+
* task 2026-09-08-caption-line-ops(issue #66)の指示 (A) / (B) / (D)。
|
|
5
|
+
*
|
|
6
|
+
* ここに置くのは決定論的な純関数だけ(DOM・時刻・乱数に依存しない = 同じ入力なら同じ操作列)。
|
|
7
|
+
* 実際の captions.json への書き戻しは呼び出し側が既存の外科手術関数
|
|
8
|
+
* (replaceCaptionLine / insertCaptionLine / removeCaptionLine / setCaptionTimingLine)へ落とす。
|
|
9
|
+
*/
|
|
10
|
+
/** 分割後・追加後の 1 行が最低限持つ表示尺(秒)。これを割るときは操作を拒否する。 */
|
|
11
|
+
export declare const CAPTION_LINE_MIN_SECONDS = 0.2;
|
|
12
|
+
/**
|
|
13
|
+
* 末尾(次の字幕が無い場所)へ字幕を足すときの既定尺(秒)。
|
|
14
|
+
* edit-lint の captions.short-duration(1.0 秒未満で warning)に自分から当たらない値を取る。
|
|
15
|
+
*/
|
|
16
|
+
export declare const CAPTION_LINE_DEFAULT_SECONDS = 1;
|
|
17
|
+
export type CaptionLineOp = {
|
|
18
|
+
readonly kind: 'replace';
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly text: string;
|
|
21
|
+
} | {
|
|
22
|
+
readonly kind: 'split';
|
|
23
|
+
readonly id: string;
|
|
24
|
+
readonly texts: string[];
|
|
25
|
+
} | {
|
|
26
|
+
readonly kind: 'merge';
|
|
27
|
+
readonly ids: string[];
|
|
28
|
+
readonly text: string;
|
|
29
|
+
} | {
|
|
30
|
+
readonly kind: 'remove';
|
|
31
|
+
readonly id: string;
|
|
32
|
+
} | {
|
|
33
|
+
readonly kind: 'insert';
|
|
34
|
+
readonly afterId: string | undefined;
|
|
35
|
+
readonly text: string;
|
|
36
|
+
};
|
|
37
|
+
export interface CaptionLineSpan {
|
|
38
|
+
start: number;
|
|
39
|
+
end: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* baseline(保存済みの表示行)と next(編集後の表示行)の差分を字幕操作列へ写す。
|
|
43
|
+
*
|
|
44
|
+
* アルゴリズムは先頭・末尾から一致行を削る古典的な差分(LCS は使わない)。中央に残った
|
|
45
|
+
* 「旧 n 行 → 新 m 行」の塊を次の順に落とす:
|
|
46
|
+
* - n === 1 && m > 1 → split
|
|
47
|
+
* - n > 1 && m === 1 → merge
|
|
48
|
+
* - それ以外 → min(n, m) 組を replace(テキストが変わった行だけ)+ 余りを remove / insert
|
|
49
|
+
*
|
|
50
|
+
* 中央の塊は 1 つしか残らないので、split と remove のような別種の操作が 1 回の呼び出しで
|
|
51
|
+
* 同時に出ることはない(連続した編集はそのたびに呼ばれる)。
|
|
52
|
+
*/
|
|
53
|
+
export declare function diffCaptionLines(baseline: readonly string[], next: readonly string[], captions: readonly {
|
|
54
|
+
id: string;
|
|
55
|
+
}[]): CaptionLineOp[];
|
|
56
|
+
/**
|
|
57
|
+
* split の時刻割り付け(指示 B-2)。元の [start, end] を分割後テキストの **文字数比** で按分し、
|
|
58
|
+
* 境界は元の区間内に収める。words があっても使わない(words の無い字幕と同じ挙動を優先する)。
|
|
59
|
+
* どれか 1 つでも minSeconds を持てないときは undefined を返す(= 分割を拒否)。
|
|
60
|
+
*/
|
|
61
|
+
export declare function planSplitSpans(start: number, end: number, texts: readonly string[], minSeconds?: number): CaptionLineSpan[] | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* insert の時刻割り付け(指示 B-5)。直前行の end から次行の start までの隙間へ count 行を置く。
|
|
64
|
+
* 隙間が count × minSeconds に足りないときは undefined を返す(= 追加を拒否)。
|
|
65
|
+
* 次行が無い(末尾への追加)ときは隙間の上限が無いので CAPTION_LINE_DEFAULT_SECONDS ずつ並べる。
|
|
66
|
+
*/
|
|
67
|
+
export declare function planInsertSpans(previousEnd: number, nextStart: number | undefined, count: number, minSeconds?: number): CaptionLineSpan[] | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* `c-0001`, `c-0002`, ... のうち既存 id と衝突しないものを順に返す採番器
|
|
70
|
+
* (timeline-material-insert.ts の nextSourceId と同型: 使用済み集合 + 衝突する限り繰り上げ)。
|
|
71
|
+
*/
|
|
72
|
+
export declare function createCaptionIdAllocator(existingIds: Iterable<string>): () => string;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 文字起こしパネル(1 行 = 1 字幕の Monaco 編集)で **行数が変わる編集** をしたときの
|
|
4
|
+
* 「行の差分 → 字幕操作列」への写像と、その操作に伴う時刻割り付け・id 採番。
|
|
5
|
+
* task 2026-09-08-caption-line-ops(issue #66)の指示 (A) / (B) / (D)。
|
|
6
|
+
*
|
|
7
|
+
* ここに置くのは決定論的な純関数だけ(DOM・時刻・乱数に依存しない = 同じ入力なら同じ操作列)。
|
|
8
|
+
* 実際の captions.json への書き戻しは呼び出し側が既存の外科手術関数
|
|
9
|
+
* (replaceCaptionLine / insertCaptionLine / removeCaptionLine / setCaptionTimingLine)へ落とす。
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.CAPTION_LINE_DEFAULT_SECONDS = exports.CAPTION_LINE_MIN_SECONDS = void 0;
|
|
13
|
+
exports.diffCaptionLines = diffCaptionLines;
|
|
14
|
+
exports.planSplitSpans = planSplitSpans;
|
|
15
|
+
exports.planInsertSpans = planInsertSpans;
|
|
16
|
+
exports.createCaptionIdAllocator = createCaptionIdAllocator;
|
|
17
|
+
/** 分割後・追加後の 1 行が最低限持つ表示尺(秒)。これを割るときは操作を拒否する。 */
|
|
18
|
+
exports.CAPTION_LINE_MIN_SECONDS = 0.2;
|
|
19
|
+
/**
|
|
20
|
+
* 末尾(次の字幕が無い場所)へ字幕を足すときの既定尺(秒)。
|
|
21
|
+
* edit-lint の captions.short-duration(1.0 秒未満で warning)に自分から当たらない値を取る。
|
|
22
|
+
*/
|
|
23
|
+
exports.CAPTION_LINE_DEFAULT_SECONDS = 1;
|
|
24
|
+
/**
|
|
25
|
+
* baseline(保存済みの表示行)と next(編集後の表示行)の差分を字幕操作列へ写す。
|
|
26
|
+
*
|
|
27
|
+
* アルゴリズムは先頭・末尾から一致行を削る古典的な差分(LCS は使わない)。中央に残った
|
|
28
|
+
* 「旧 n 行 → 新 m 行」の塊を次の順に落とす:
|
|
29
|
+
* - n === 1 && m > 1 → split
|
|
30
|
+
* - n > 1 && m === 1 → merge
|
|
31
|
+
* - それ以外 → min(n, m) 組を replace(テキストが変わった行だけ)+ 余りを remove / insert
|
|
32
|
+
*
|
|
33
|
+
* 中央の塊は 1 つしか残らないので、split と remove のような別種の操作が 1 回の呼び出しで
|
|
34
|
+
* 同時に出ることはない(連続した編集はそのたびに呼ばれる)。
|
|
35
|
+
*/
|
|
36
|
+
function diffCaptionLines(baseline, next, captions) {
|
|
37
|
+
if (baseline.length !== captions.length) {
|
|
38
|
+
throw new Error('字幕の行数と字幕データの件数が一致しません。');
|
|
39
|
+
}
|
|
40
|
+
let prefix = 0;
|
|
41
|
+
while (prefix < baseline.length && prefix < next.length && baseline[prefix] === next[prefix]) {
|
|
42
|
+
prefix++;
|
|
43
|
+
}
|
|
44
|
+
let suffix = 0;
|
|
45
|
+
while (suffix < baseline.length - prefix && suffix < next.length - prefix
|
|
46
|
+
&& baseline[baseline.length - 1 - suffix] === next[next.length - 1 - suffix]) {
|
|
47
|
+
suffix++;
|
|
48
|
+
}
|
|
49
|
+
const oldCount = baseline.length - prefix - suffix;
|
|
50
|
+
const newCount = next.length - prefix - suffix;
|
|
51
|
+
if (oldCount === 0 && newCount === 0) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const ids = captions.slice(prefix, prefix + oldCount).map(caption => caption.id);
|
|
55
|
+
const texts = next.slice(prefix, prefix + newCount);
|
|
56
|
+
if (oldCount === 1 && newCount > 1) {
|
|
57
|
+
return [{ kind: 'split', id: ids[0], texts }];
|
|
58
|
+
}
|
|
59
|
+
if (oldCount > 1 && newCount === 1) {
|
|
60
|
+
return [{ kind: 'merge', ids, text: texts[0] }];
|
|
61
|
+
}
|
|
62
|
+
const ops = [];
|
|
63
|
+
const paired = Math.min(oldCount, newCount);
|
|
64
|
+
for (let index = 0; index < paired; index++) {
|
|
65
|
+
if (texts[index] !== baseline[prefix + index]) {
|
|
66
|
+
ops.push({ kind: 'replace', id: ids[index], text: texts[index] });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (let index = paired; index < oldCount; index++) {
|
|
70
|
+
ops.push({ kind: 'remove', id: ids[index] });
|
|
71
|
+
}
|
|
72
|
+
if (newCount > paired) {
|
|
73
|
+
// 追加行を差し込む位置は「直前に残る既存行」で表す。塊の中で組になった行があれば
|
|
74
|
+
// その最後、無ければ塊の直前(先頭挿入なら undefined)。
|
|
75
|
+
const afterId = paired > 0
|
|
76
|
+
? ids[paired - 1]
|
|
77
|
+
: prefix > 0 ? captions[prefix - 1].id : undefined;
|
|
78
|
+
for (let index = paired; index < newCount; index++) {
|
|
79
|
+
ops.push({ kind: 'insert', afterId, text: texts[index] });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return ops;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* split の時刻割り付け(指示 B-2)。元の [start, end] を分割後テキストの **文字数比** で按分し、
|
|
86
|
+
* 境界は元の区間内に収める。words があっても使わない(words の無い字幕と同じ挙動を優先する)。
|
|
87
|
+
* どれか 1 つでも minSeconds を持てないときは undefined を返す(= 分割を拒否)。
|
|
88
|
+
*/
|
|
89
|
+
function planSplitSpans(start, end, texts, minSeconds = exports.CAPTION_LINE_MIN_SECONDS) {
|
|
90
|
+
if (texts.length < 2 || !Number.isFinite(start) || !Number.isFinite(end) || end <= start) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const duration = end - start;
|
|
94
|
+
if (duration < minSeconds * texts.length - EPSILON) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
// 文字数は code point 単位で数える(サロゲートペアで比が壊れないように)。
|
|
98
|
+
const weights = texts.map(text => Math.max(1, Array.from(text).length));
|
|
99
|
+
const total = weights.reduce((sum, weight) => sum + weight, 0);
|
|
100
|
+
const spans = [];
|
|
101
|
+
let cursor = start;
|
|
102
|
+
let consumed = 0;
|
|
103
|
+
for (let index = 0; index < texts.length; index++) {
|
|
104
|
+
consumed += weights[index];
|
|
105
|
+
const boundary = index === texts.length - 1
|
|
106
|
+
? end
|
|
107
|
+
: Math.min(end, Math.max(cursor, roundMilliseconds(start + duration * consumed / total)));
|
|
108
|
+
spans.push({ start: cursor, end: boundary });
|
|
109
|
+
cursor = boundary;
|
|
110
|
+
}
|
|
111
|
+
return spans.every(span => span.end - span.start >= minSeconds - EPSILON) ? spans : undefined;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* insert の時刻割り付け(指示 B-5)。直前行の end から次行の start までの隙間へ count 行を置く。
|
|
115
|
+
* 隙間が count × minSeconds に足りないときは undefined を返す(= 追加を拒否)。
|
|
116
|
+
* 次行が無い(末尾への追加)ときは隙間の上限が無いので CAPTION_LINE_DEFAULT_SECONDS ずつ並べる。
|
|
117
|
+
*/
|
|
118
|
+
function planInsertSpans(previousEnd, nextStart, count, minSeconds = exports.CAPTION_LINE_MIN_SECONDS) {
|
|
119
|
+
if (count < 1 || !Number.isFinite(previousEnd) || previousEnd < 0) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
if (nextStart === undefined) {
|
|
123
|
+
return Array.from({ length: count }, (_unused, index) => ({
|
|
124
|
+
start: roundMilliseconds(previousEnd + exports.CAPTION_LINE_DEFAULT_SECONDS * index),
|
|
125
|
+
end: roundMilliseconds(previousEnd + exports.CAPTION_LINE_DEFAULT_SECONDS * (index + 1))
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
const gap = nextStart - previousEnd;
|
|
129
|
+
if (!Number.isFinite(gap) || gap < minSeconds * count - EPSILON) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
const slot = gap / count;
|
|
133
|
+
const duration = Math.min(slot, exports.CAPTION_LINE_DEFAULT_SECONDS);
|
|
134
|
+
return Array.from({ length: count }, (_unused, index) => {
|
|
135
|
+
const spanStart = roundMilliseconds(previousEnd + slot * index);
|
|
136
|
+
return {
|
|
137
|
+
start: spanStart,
|
|
138
|
+
end: Math.min(nextStart, roundMilliseconds(spanStart + duration))
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* `c-0001`, `c-0002`, ... のうち既存 id と衝突しないものを順に返す採番器
|
|
144
|
+
* (timeline-material-insert.ts の nextSourceId と同型: 使用済み集合 + 衝突する限り繰り上げ)。
|
|
145
|
+
*/
|
|
146
|
+
function createCaptionIdAllocator(existingIds) {
|
|
147
|
+
const used = new Set(existingIds);
|
|
148
|
+
let serial = 1;
|
|
149
|
+
for (const id of used) {
|
|
150
|
+
const match = /^c-(\d{4,})$/.exec(id);
|
|
151
|
+
if (match) {
|
|
152
|
+
serial = Math.max(serial, Number(match[1]) + 1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return () => {
|
|
156
|
+
let candidate;
|
|
157
|
+
do {
|
|
158
|
+
candidate = `c-${String(serial++).padStart(4, '0')}`;
|
|
159
|
+
} while (used.has(candidate));
|
|
160
|
+
used.add(candidate);
|
|
161
|
+
return candidate;
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const EPSILON = 1e-9;
|
|
165
|
+
function roundMilliseconds(value) {
|
|
166
|
+
return Math.round(value * 1000) / 1000;
|
|
167
|
+
}
|
|
@@ -22,47 +22,48 @@ async function loadDependencies() {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const usage = "使い方: node skills/manage-connections/bin/doctor.mjs [プロジェクトルート|connections.json]";
|
|
25
|
-
const
|
|
25
|
+
export const timeoutMs = 5_000;
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
process.exit(2);
|
|
30
|
-
}
|
|
27
|
+
async function main() {
|
|
28
|
+
const argument = process.argv[2];
|
|
31
29
|
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
30
|
+
if (process.argv.length > 3) {
|
|
31
|
+
console.error(usage);
|
|
32
|
+
process.exit(2);
|
|
33
|
+
}
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const projectRoot = inputPath.endsWith(".json")
|
|
42
|
-
? path.dirname(path.dirname(connectionsPath))
|
|
43
|
-
: inputPath;
|
|
44
|
-
const reportPath = path.join(projectRoot, "connections-report.html");
|
|
45
|
-
const credentialsPath = path.resolve(
|
|
46
|
-
process.env.AKARI_CREDENTIALS_FILE ?? path.join(os.homedir(), ".config", "akari-video", "credentials.env"),
|
|
47
|
-
);
|
|
48
|
-
const timeoutMs = 5_000;
|
|
35
|
+
if (argument === "--help" || argument === "-h") {
|
|
36
|
+
console.log(usage);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
49
39
|
|
|
50
|
-
|
|
40
|
+
const inputPath = path.resolve(argument ?? process.cwd());
|
|
41
|
+
const connectionsPath = inputPath.endsWith(".json")
|
|
42
|
+
? inputPath
|
|
43
|
+
: path.join(inputPath, ".akari", "connections.json");
|
|
44
|
+
const projectRoot = inputPath.endsWith(".json")
|
|
45
|
+
? path.dirname(path.dirname(connectionsPath))
|
|
46
|
+
: inputPath;
|
|
47
|
+
const reportPath = path.join(projectRoot, "connections-report.html");
|
|
48
|
+
const credentialsPath = path.resolve(
|
|
49
|
+
process.env.AKARI_CREDENTIALS_FILE ?? path.join(os.homedir(), ".config", "akari-video", "credentials.env"),
|
|
50
|
+
);
|
|
51
51
|
await loadDependencies();
|
|
52
|
+
const context = { inputPath, connectionsPath, projectRoot, reportPath, credentialsPath };
|
|
52
53
|
if (inputPath.endsWith(".json")) {
|
|
53
|
-
await mainLegacy();
|
|
54
|
+
await mainLegacy(context);
|
|
54
55
|
return;
|
|
55
56
|
}
|
|
56
|
-
await mainResolved();
|
|
57
|
+
await mainResolved(context);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
async function mainLegacy() {
|
|
60
|
-
const registry = readRegistry();
|
|
61
|
-
const credentialState = readCredentials();
|
|
60
|
+
async function mainLegacy({ connectionsPath, reportPath, credentialsPath }) {
|
|
61
|
+
const registry = readRegistry(connectionsPath);
|
|
62
|
+
const credentialState = readCredentials(credentialsPath);
|
|
62
63
|
const checkedAt = new Date().toISOString();
|
|
63
64
|
|
|
64
65
|
if (!credentialState.exists) {
|
|
65
|
-
printCredentialGuide(registry);
|
|
66
|
+
printCredentialGuide(registry, credentialsPath);
|
|
66
67
|
} else if (!credentialState.securePermissions) {
|
|
67
68
|
console.warn(`警告: credentials.env の権限は 600 ではありません(現在 ${credentialState.mode})。chmod 600 ${credentialsPath} を実行してください。`);
|
|
68
69
|
}
|
|
@@ -74,7 +75,7 @@ async function mainLegacy() {
|
|
|
74
75
|
|
|
75
76
|
const jsonOutput = `${JSON.stringify(registry, null, 2)}\n`;
|
|
76
77
|
refuseSecretLeak(jsonOutput, credentialState.values);
|
|
77
|
-
const htmlOutput = renderReport(registry, results, credentialState, checkedAt);
|
|
78
|
+
const htmlOutput = renderReport(registry, results, credentialState, checkedAt, credentialsPath);
|
|
78
79
|
refuseSecretLeak(htmlOutput, credentialState.values);
|
|
79
80
|
|
|
80
81
|
fs.writeFileSync(connectionsPath, jsonOutput, "utf8");
|
|
@@ -86,14 +87,14 @@ async function mainLegacy() {
|
|
|
86
87
|
console.log(`接続レポート: ${reportPath}`);
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
async function mainResolved() {
|
|
90
|
+
async function mainResolved({ projectRoot, reportPath, credentialsPath }) {
|
|
90
91
|
const resolved = await resolveConnections({ projectRoot });
|
|
91
92
|
const registry = clone(resolved.effective);
|
|
92
|
-
const credentialState = readCredentials();
|
|
93
|
+
const credentialState = readCredentials(credentialsPath);
|
|
93
94
|
const checkedAt = new Date().toISOString();
|
|
94
95
|
|
|
95
96
|
if (!credentialState.exists) {
|
|
96
|
-
printCredentialGuide(registry);
|
|
97
|
+
printCredentialGuide(registry, credentialsPath);
|
|
97
98
|
} else if (!credentialState.securePermissions) {
|
|
98
99
|
console.warn(`警告: credentials.env の権限は 600 ではありません(現在 ${credentialState.mode})。chmod 600 ${credentialsPath} を実行してください。`);
|
|
99
100
|
}
|
|
@@ -129,7 +130,7 @@ async function mainResolved() {
|
|
|
129
130
|
writeRegistry(resolved.layers.workspace.path, workspaceRegistry, credentialState.values);
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
const htmlOutput = renderReport(registry, results, credentialState, checkedAt);
|
|
133
|
+
const htmlOutput = renderReport(registry, results, credentialState, checkedAt, credentialsPath);
|
|
133
134
|
refuseSecretLeak(htmlOutput, credentialState.values);
|
|
134
135
|
fs.writeFileSync(reportPath, htmlOutput, "utf8");
|
|
135
136
|
|
|
@@ -144,7 +145,7 @@ async function mainResolved() {
|
|
|
144
145
|
console.log(`接続レポート: ${reportPath}`);
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
function readRegistry() {
|
|
148
|
+
function readRegistry(connectionsPath) {
|
|
148
149
|
let value;
|
|
149
150
|
try {
|
|
150
151
|
value = JSON.parse(fs.readFileSync(connectionsPath, "utf8"));
|
|
@@ -198,7 +199,7 @@ function clone(value) {
|
|
|
198
199
|
return JSON.parse(JSON.stringify(value));
|
|
199
200
|
}
|
|
200
201
|
|
|
201
|
-
function readCredentials() {
|
|
202
|
+
function readCredentials(credentialsPath) {
|
|
202
203
|
let stat;
|
|
203
204
|
try {
|
|
204
205
|
stat = fs.statSync(credentialsPath);
|
|
@@ -277,7 +278,7 @@ async function inspectProvider(provider, credentialState, checkedAt) {
|
|
|
277
278
|
return { id, configured: true, doctor };
|
|
278
279
|
}
|
|
279
280
|
|
|
280
|
-
const adapters = {
|
|
281
|
+
export const adapters = {
|
|
281
282
|
fal: (secret, checkedAt) =>
|
|
282
283
|
checkGet("https://rest.alpha.fal.ai/billing/user_balance", { Authorization: `Key ${secret}` }, checkedAt),
|
|
283
284
|
replicate: (secret, checkedAt) =>
|
|
@@ -375,7 +376,7 @@ function extractEnvName(value) {
|
|
|
375
376
|
return match ? match[1] : null;
|
|
376
377
|
}
|
|
377
378
|
|
|
378
|
-
function printCredentialGuide(registry) {
|
|
379
|
+
function printCredentialGuide(registry, credentialsPath) {
|
|
379
380
|
console.log(`credentials.env がありません: ${credentialsPath}`);
|
|
380
381
|
console.log(`作成後に chmod 600 ${credentialsPath} を実行してください。キーは人間が取得して配置してください。`);
|
|
381
382
|
for (const provider of registry.providers) {
|
|
@@ -394,7 +395,7 @@ function refuseSecretLeak(output, values) {
|
|
|
394
395
|
}
|
|
395
396
|
}
|
|
396
397
|
|
|
397
|
-
function renderReport(registry, results, credentialState, checkedAt) {
|
|
398
|
+
function renderReport(registry, results, credentialState, checkedAt, credentialsPath) {
|
|
398
399
|
const resultById = new Map(results.map((item) => [item.id, item]));
|
|
399
400
|
const cards = registry.providers.map((provider) => {
|
|
400
401
|
const result = resultById.get(provider.id);
|