akari-video 0.1.65 → 0.1.66
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/.akari-capability-sources.json +2 -0
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/edit-store/lib/generation-meta-node.d.ts +11 -0
- package/vendor/packages/edit-store/lib/generation-meta-node.js +101 -0
- package/vendor/packages/edit-store/lib/generation-meta.d.ts +42 -0
- package/vendor/packages/edit-store/lib/generation-meta.js +33 -0
- 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/package.json +1 -0
- package/vendor/packages/generate/README.md +12 -0
- package/vendor/packages/generate/package.json +11 -0
- package/vendor/packages/schemas/bin/refresh-gen-models-openapi.mjs +118 -0
- package/vendor/packages/schemas/bin/validate-captions.mjs +8 -1
- package/vendor/packages/schemas/bin/validate-gen-models.mjs +145 -0
- package/vendor/packages/schemas/bin/validate-generation-meta.mjs +75 -0
- package/vendor/packages/schemas/captions.schema.json +3 -1
- package/vendor/packages/schemas/fixtures/gen-models/openapi/fal_grok-imagine-i2v.json +414 -0
- package/vendor/packages/schemas/fixtures/gen-models/openapi/fal_h3-i2v.json +401 -0
- package/vendor/packages/schemas/fixtures/gen-models/openapi/fal_h3-ref.json +421 -0
- package/vendor/packages/schemas/fixtures/gen-models/openapi/fal_kling-v3-pro-i2v.json +578 -0
- package/vendor/packages/schemas/fixtures/gen-models/openapi/fal_kling-v3-standard-i2v.json +578 -0
- package/vendor/packages/schemas/fixtures/gen-models/openapi/fal_nano-banana-pro-edit.json +463 -0
- package/vendor/packages/schemas/fixtures/gen-models/openapi/fal_seedance-2.0-i2v.json +406 -0
- package/vendor/packages/schemas/fixtures/gen-models/openapi/fal_seedance-2.0-ref.json +418 -0
- package/vendor/packages/schemas/fixtures/gen-models/openapi/fal_seedance-2.5-i2v.json +412 -0
- package/vendor/packages/schemas/fixtures/gen-models/openapi/fal_veo-3.1-flf.json +403 -0
- package/vendor/packages/schemas/fixtures/gen-models/openapi/fal_veo-3.1-ref.json +368 -0
- package/vendor/packages/schemas/fixtures/gen-models/openapi/fal_vidu-q3-i2v.json +360 -0
- package/vendor/packages/schemas/fixtures/gen-models/openapi/fal_wan-2.7-i2v.json +533 -0
- package/vendor/packages/schemas/fixtures/generation-meta/done.json +80 -0
- package/vendor/packages/schemas/fixtures/generation-meta/failed.json +18 -0
- package/vendor/packages/schemas/fixtures/generation-meta/generating.json +18 -0
- package/vendor/packages/schemas/fixtures/generation-meta/planned.json +25 -0
- package/vendor/packages/schemas/fixtures/generation-meta/stale.json +18 -0
- package/vendor/packages/schemas/fixtures/generation-meta/still.json +20 -0
- package/vendor/packages/schemas/gen-models.json +425 -0
- package/vendor/packages/schemas/gen-models.schema.json +207 -0
- package/vendor/packages/schemas/generation-meta.schema.json +170 -0
- package/vendor/packages/schemas/test/gen-models-drift.test.mjs +286 -0
- package/vendor/packages/schemas/test/gen-models.test.mjs +84 -0
- package/vendor/packages/schemas/test/generation-meta.test.mjs +72 -0
- package/vendor/packages/schemas/test/validate-captions.test.mjs +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.66",
|
|
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": {
|
|
@@ -91,6 +91,8 @@
|
|
|
91
91
|
"packages/frame-engine/package.json",
|
|
92
92
|
"packages/frame-engine/README.ja.md",
|
|
93
93
|
"packages/frame-engine/README.md",
|
|
94
|
+
"packages/generate/package.json",
|
|
95
|
+
"packages/generate/README.md",
|
|
94
96
|
"packages/gpu-export/package.json",
|
|
95
97
|
"packages/gpu-export/README.ja.md",
|
|
96
98
|
"packages/gpu-export/README.md",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.66",
|
|
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": [
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { GenerationMetaV1, ReadGenerationMetaResult } from './generation-meta';
|
|
2
|
+
export type { GenerationBinding, GenerationMetaV1, GenerationState, ReadGenerationMetaResult, } from './generation-meta';
|
|
3
|
+
export declare function readGenerationMeta(options: {
|
|
4
|
+
projectRoot: string;
|
|
5
|
+
sourcePath: string;
|
|
6
|
+
now: Date | string | number;
|
|
7
|
+
}): ReadGenerationMetaResult;
|
|
8
|
+
export declare function findGenerationMetaBySha(options: {
|
|
9
|
+
projectRoot: string;
|
|
10
|
+
sha256: string;
|
|
11
|
+
}): GenerationMetaV1 | null;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readGenerationMeta = readGenerationMeta;
|
|
4
|
+
exports.findGenerationMetaBySha = findGenerationMetaBySha;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const fs = require("node:fs");
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
const generation_meta_1 = require("./generation-meta");
|
|
9
|
+
function readGenerationMeta(options) {
|
|
10
|
+
const sourcePath = resolveInsideProject(options.projectRoot, options.sourcePath);
|
|
11
|
+
const sidecarPath = (0, generation_meta_1.sidecarPathFor)(sourcePath);
|
|
12
|
+
if (!fs.existsSync(sidecarPath)) {
|
|
13
|
+
return { state: 'none', meta: null, sidecarPath, binding: null };
|
|
14
|
+
}
|
|
15
|
+
const meta = parseMeta(sidecarPath);
|
|
16
|
+
const expected = bindingSha(meta);
|
|
17
|
+
let binding = null;
|
|
18
|
+
let state = (0, generation_meta_1.resolveGenerationState)(meta, options.now);
|
|
19
|
+
if (expected) {
|
|
20
|
+
const actualSha256 = fs.existsSync(sourcePath) && fs.statSync(sourcePath).isFile()
|
|
21
|
+
? sha256File(sourcePath)
|
|
22
|
+
: null;
|
|
23
|
+
binding = {
|
|
24
|
+
expectedSha256: expected.sha256,
|
|
25
|
+
actualSha256,
|
|
26
|
+
matches: actualSha256 === expected.sha256,
|
|
27
|
+
source: expected.source,
|
|
28
|
+
};
|
|
29
|
+
if (!binding.matches)
|
|
30
|
+
state = 'orphan';
|
|
31
|
+
}
|
|
32
|
+
return { state, meta, sidecarPath, binding };
|
|
33
|
+
}
|
|
34
|
+
function findGenerationMetaBySha(options) {
|
|
35
|
+
const generatedRoot = resolveInsideProject(options.projectRoot, 'assets/generated');
|
|
36
|
+
if (!fs.existsSync(generatedRoot) || !fs.statSync(generatedRoot).isDirectory())
|
|
37
|
+
return null;
|
|
38
|
+
for (const sidecarPath of generationSidecars(generatedRoot)) {
|
|
39
|
+
const meta = parseMeta(sidecarPath);
|
|
40
|
+
const expected = bindingSha(meta);
|
|
41
|
+
if (expected?.sha256 === options.sha256)
|
|
42
|
+
return meta;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function bindingSha(meta) {
|
|
47
|
+
if (meta.status === 'done' && typeof meta.result?.sha256 === 'string') {
|
|
48
|
+
return { sha256: meta.result.sha256, source: 'result' };
|
|
49
|
+
}
|
|
50
|
+
if ((meta.kind === 'still' || meta.status === 'planned') && typeof meta.inputs?.first_frame?.sha256 === 'string') {
|
|
51
|
+
return { sha256: meta.inputs.first_frame.sha256, source: 'first_frame' };
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
function generationSidecars(directory) {
|
|
56
|
+
const found = [];
|
|
57
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
58
|
+
const absolute = path.join(directory, entry.name);
|
|
59
|
+
if (entry.isDirectory())
|
|
60
|
+
found.push(...generationSidecars(absolute));
|
|
61
|
+
else if (entry.isFile() && entry.name.endsWith('.meta.json'))
|
|
62
|
+
found.push(absolute);
|
|
63
|
+
}
|
|
64
|
+
return found.sort();
|
|
65
|
+
}
|
|
66
|
+
function resolveInsideProject(projectRoot, candidate) {
|
|
67
|
+
if (!candidate)
|
|
68
|
+
throw new Error('sourcePath は空でないパスである必要があります。');
|
|
69
|
+
const root = path.resolve(projectRoot);
|
|
70
|
+
const absolute = path.isAbsolute(candidate) ? path.resolve(candidate) : path.resolve(root, candidate);
|
|
71
|
+
const relative = path.relative(root, absolute);
|
|
72
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
73
|
+
throw new Error(`projectRoot 外のパスは扱えません: ${candidate}`);
|
|
74
|
+
}
|
|
75
|
+
if (fs.existsSync(absolute)) {
|
|
76
|
+
const realRoot = fs.realpathSync(root);
|
|
77
|
+
const realPath = fs.realpathSync(absolute);
|
|
78
|
+
const realRelative = path.relative(realRoot, realPath);
|
|
79
|
+
if (realRelative === '..' || realRelative.startsWith(`..${path.sep}`) || path.isAbsolute(realRelative)) {
|
|
80
|
+
throw new Error(`projectRoot 外を指すパスは扱えません: ${candidate}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return absolute;
|
|
84
|
+
}
|
|
85
|
+
function parseMeta(sidecarPath) {
|
|
86
|
+
let value;
|
|
87
|
+
try {
|
|
88
|
+
value = JSON.parse(fs.readFileSync(sidecarPath, 'utf8'));
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
92
|
+
throw new Error(`生成サイドカーを読めません: ${sidecarPath}: ${message}`);
|
|
93
|
+
}
|
|
94
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
95
|
+
throw new Error(`生成サイドカーのルートは object である必要があります: ${sidecarPath}`);
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
function sha256File(filePath) {
|
|
100
|
+
return (0, node_crypto_1.createHash)('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
101
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ブラウザ安全な生成サイドカーの型と純粋関数。
|
|
3
|
+
* fs / crypto を使う読み取りは './generation-meta-node' を明示的に import すること
|
|
4
|
+
* (ここへ Node 専用依存を戻すと browser バンドルに node builtins が混入するため分離している)。
|
|
5
|
+
*/
|
|
6
|
+
export type GenerationState = 'none' | 'planned' | 'generating' | 'stale' | 'done' | 'failed' | 'orphan';
|
|
7
|
+
export interface GenerationMetaV1 {
|
|
8
|
+
version: 1;
|
|
9
|
+
kind: 'still' | 'video' | 'frames';
|
|
10
|
+
status: 'planned' | 'generating' | 'done' | 'failed';
|
|
11
|
+
inputs?: {
|
|
12
|
+
first_frame?: {
|
|
13
|
+
sha256?: string;
|
|
14
|
+
} | null;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
job?: {
|
|
18
|
+
started_at?: string;
|
|
19
|
+
stale_after_s?: number;
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
};
|
|
22
|
+
result?: {
|
|
23
|
+
sha256?: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
};
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
export interface GenerationBinding {
|
|
29
|
+
expectedSha256: string;
|
|
30
|
+
actualSha256: string | null;
|
|
31
|
+
matches: boolean;
|
|
32
|
+
source: 'result' | 'first_frame';
|
|
33
|
+
}
|
|
34
|
+
export interface ReadGenerationMetaResult {
|
|
35
|
+
state: GenerationState;
|
|
36
|
+
meta: GenerationMetaV1 | null;
|
|
37
|
+
sidecarPath: string;
|
|
38
|
+
binding: GenerationBinding | null;
|
|
39
|
+
}
|
|
40
|
+
export declare function sidecarPathFor(sourcePath: string): string;
|
|
41
|
+
/** fs に触れず、サイドカー自身が表す状態だけを解決する。 */
|
|
42
|
+
export declare function resolveGenerationState(meta: GenerationMetaV1 | null | undefined, now: Date | string | number): GenerationState;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ブラウザ安全な生成サイドカーの型と純粋関数。
|
|
4
|
+
* fs / crypto を使う読み取りは './generation-meta-node' を明示的に import すること
|
|
5
|
+
* (ここへ Node 専用依存を戻すと browser バンドルに node builtins が混入するため分離している)。
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.sidecarPathFor = sidecarPathFor;
|
|
9
|
+
exports.resolveGenerationState = resolveGenerationState;
|
|
10
|
+
function sidecarPathFor(sourcePath) {
|
|
11
|
+
return `${sourcePath}.meta.json`;
|
|
12
|
+
}
|
|
13
|
+
/** fs に触れず、サイドカー自身が表す状態だけを解決する。 */
|
|
14
|
+
function resolveGenerationState(meta, now) {
|
|
15
|
+
if (!meta)
|
|
16
|
+
return 'none';
|
|
17
|
+
if (meta.status === 'failed')
|
|
18
|
+
return 'failed';
|
|
19
|
+
if (meta.status === 'generating') {
|
|
20
|
+
const nowMs = timeValue(now);
|
|
21
|
+
const startedMs = Date.parse(String(meta.job?.started_at ?? ''));
|
|
22
|
+
const staleAfterS = meta.job?.stale_after_s;
|
|
23
|
+
if (Number.isFinite(nowMs)
|
|
24
|
+
&& Number.isFinite(startedMs)
|
|
25
|
+
&& typeof staleAfterS === 'number'
|
|
26
|
+
&& nowMs - startedMs > staleAfterS * 1000)
|
|
27
|
+
return 'stale';
|
|
28
|
+
}
|
|
29
|
+
return meta.status;
|
|
30
|
+
}
|
|
31
|
+
function timeValue(value) {
|
|
32
|
+
return value instanceof Date ? value.getTime() : typeof value === 'number' ? value : Date.parse(value);
|
|
33
|
+
}
|
|
@@ -12,6 +12,7 @@ export * from './caption-window';
|
|
|
12
12
|
export * from './caption-clock';
|
|
13
13
|
export * from './timeline-map';
|
|
14
14
|
export * from './caption-display';
|
|
15
|
+
export * from './generation-meta';
|
|
15
16
|
export * from './edit-v2';
|
|
16
17
|
export * from './edit-v2-item-write';
|
|
17
18
|
export * from './internal-model';
|
|
@@ -29,6 +29,7 @@ __exportStar(require("./caption-window"), exports);
|
|
|
29
29
|
__exportStar(require("./caption-clock"), exports);
|
|
30
30
|
__exportStar(require("./timeline-map"), exports);
|
|
31
31
|
__exportStar(require("./caption-display"), exports);
|
|
32
|
+
__exportStar(require("./generation-meta"), exports);
|
|
32
33
|
// Includes AdjustV1 and its curve/wheel/hue types; AdjustV0 remains an alias.
|
|
33
34
|
__exportStar(require("./edit-v2"), exports);
|
|
34
35
|
__exportStar(require("./edit-v2-item-write"), exports);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
".": "./lib/index.js",
|
|
10
10
|
"./lib/project": "./lib/project.js",
|
|
11
11
|
"./lib/write-gate": "./lib/write-gate.js",
|
|
12
|
+
"./lib/generation-meta-node": "./lib/generation-meta-node.js",
|
|
12
13
|
"./lib/migrate": "./lib/migrate/index.js",
|
|
13
14
|
"./lib/*.js": "./lib/*.js",
|
|
14
15
|
"./lib/*": "./lib/*.js",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# @akari-video/generate
|
|
2
|
+
|
|
3
|
+
This dependency-free package normalizes generation slots and validates them against one model capability row.
|
|
4
|
+
The UI, CLI, and agent integrations should all call the same `validateInputs` function.
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
import { validateInputs } from '@akari-video/generate';
|
|
8
|
+
const { ok, normalized, rounded, messages, cost } = validateInputs({ inputs, output, model });
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Messages use `error`, `warn`, or `info` levels with stable codes such as `first_frame.required`, `duration.rounded`, and `price.unknown`.
|
|
12
|
+
A model without recorded pricing remains valid and returns `cost.needs_explicit_confirm` instead of changing `ok` to false.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@akari-video/generate",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "生成入力(9 スロット + 出力ノブ)を能力カタログ行に対して検証・正規化・丸め・費用見積する依存ゼロの共有モジュール。UI / CLI / エージェントが同じ関数を使う。",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "src/index.mjs",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "node --test \"test/*.test.mjs\""
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// gen-models.json の fal 行について、明示実行時だけ OpenAPI スナップショットを更新する。
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const usage = "使い方: node packages/schemas/bin/refresh-gen-models-openapi.mjs";
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
|
|
12
|
+
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
|
|
13
|
+
console.log(usage);
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
if (args.length !== 0) {
|
|
17
|
+
console.error(usage);
|
|
18
|
+
process.exit(2);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const packageRoot = fileURLToPath(new URL("..", import.meta.url));
|
|
22
|
+
const catalogPath = path.join(packageRoot, "gen-models.json");
|
|
23
|
+
const fixtureRoot = path.join(packageRoot, "fixtures", "gen-models", "openapi");
|
|
24
|
+
|
|
25
|
+
let catalog;
|
|
26
|
+
try {
|
|
27
|
+
catalog = JSON.parse(fs.readFileSync(catalogPath, "utf8"));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(`ERROR: gen-models.json を読めません: ${messageOf(error)}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const models = catalog.models.filter((model) => model.provider === "fal");
|
|
34
|
+
fs.mkdirSync(fixtureRoot, { recursive: true });
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
for (const model of models) {
|
|
38
|
+
const response = await fetch(model.source_url, { headers: { accept: "application/json" } });
|
|
39
|
+
if (!response.ok) throw new Error(`${model.id}: HTTP ${response.status} ${response.statusText}`);
|
|
40
|
+
const document = await response.json();
|
|
41
|
+
sanitizeDocument(document);
|
|
42
|
+
const outputPath = path.join(fixtureRoot, `${model.id.replaceAll(":", "_")}.json`);
|
|
43
|
+
const temporaryPath = `${outputPath}.tmp`;
|
|
44
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify(document, null, 2)}\n`, "utf8");
|
|
45
|
+
fs.renameSync(temporaryPath, outputPath);
|
|
46
|
+
console.log(`更新: ${model.id} -> ${outputPath}`);
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error(`ERROR: OpenAPI の更新に失敗しました: ${messageOf(error)}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(`OK: fal ${models.length} 行の OpenAPI を更新しました`);
|
|
54
|
+
|
|
55
|
+
function sanitizeDocument(document) {
|
|
56
|
+
// fal のメタデータに含まれる表示用サムネイルは、スナップショットへ保存しない。
|
|
57
|
+
delete document.info?.["x-fal-metadata"]?.thumbnailUrl;
|
|
58
|
+
|
|
59
|
+
// 名前マップ直下では同名の property / schema を保護し、それ以外の例示キーを落とす。
|
|
60
|
+
removeExampleKeys(document, []);
|
|
61
|
+
|
|
62
|
+
// 構造上必要な配信先だけを残し、説明文などに埋め込まれた URL も固定値へ置換する。
|
|
63
|
+
replaceUrls(document, []);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function removeExampleKeys(value, pathParts) {
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
69
|
+
removeExampleKeys(value[index], [...pathParts, index]);
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (value === null || typeof value !== "object") return;
|
|
74
|
+
|
|
75
|
+
const namedMapKeys = new Set(["properties", "schemas", "$defs", "definitions"]);
|
|
76
|
+
const protectedNameMap = namedMapKeys.has(pathParts.at(-1));
|
|
77
|
+
for (const key of Object.keys(value)) {
|
|
78
|
+
if (!protectedNameMap && ["examples", "example", "externalDocs"].includes(key)) {
|
|
79
|
+
delete value[key];
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
removeExampleKeys(value[key], [...pathParts, key]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function replaceUrls(value, pathParts) {
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
89
|
+
if (typeof value[index] === "string" && !isServerUrl([...pathParts, index])) {
|
|
90
|
+
value[index] = value[index].replace(/https?:\/\/[^\s<>"'`)\]}]+/g, "<url-removed>");
|
|
91
|
+
} else {
|
|
92
|
+
replaceUrls(value[index], [...pathParts, index]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (value === null || typeof value !== "object") return;
|
|
98
|
+
|
|
99
|
+
for (const key of Object.keys(value)) {
|
|
100
|
+
const childPath = [...pathParts, key];
|
|
101
|
+
if (typeof value[key] === "string" && !isServerUrl(childPath)) {
|
|
102
|
+
value[key] = value[key].replace(/https?:\/\/[^\s<>"'`)\]}]+/g, "<url-removed>");
|
|
103
|
+
} else {
|
|
104
|
+
replaceUrls(value[key], childPath);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isServerUrl(pathParts) {
|
|
110
|
+
return pathParts.length === 3
|
|
111
|
+
&& pathParts[0] === "servers"
|
|
112
|
+
&& Number.isInteger(pathParts[1])
|
|
113
|
+
&& pathParts[2] === "url";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function messageOf(error) {
|
|
117
|
+
return error instanceof Error ? error.message : String(error);
|
|
118
|
+
}
|
|
@@ -299,13 +299,20 @@ function validateCaptionWords(value, captionLabel) {
|
|
|
299
299
|
if (!hasOwn(word, field)) fail(`${label}.${field} は必須です`);
|
|
300
300
|
}
|
|
301
301
|
for (const key of Object.keys(word)) {
|
|
302
|
-
if (!["start", "end", "text"].includes(key)) fail(`${label} に未知のキーがあります: ${key}`);
|
|
302
|
+
if (!["start", "end", "raw_start", "raw_end", "text"].includes(key)) fail(`${label} に未知のキーがあります: ${key}`);
|
|
303
303
|
}
|
|
304
304
|
const startValid = isFiniteNumber(word.start) && word.start >= 0;
|
|
305
305
|
const endValid = isFiniteNumber(word.end) && word.end >= 0;
|
|
306
306
|
if (!startValid || !endValid || word.end < word.start) {
|
|
307
307
|
fail(`${label} は 0 <= start <= end を満たす必要があります`);
|
|
308
308
|
}
|
|
309
|
+
const hasRawStart = hasOwn(word, "raw_start");
|
|
310
|
+
const hasRawEnd = hasOwn(word, "raw_end");
|
|
311
|
+
if (hasRawStart !== hasRawEnd) fail(`${label}.raw_start / raw_end は両方指定してください`);
|
|
312
|
+
if (hasRawStart && (!isFiniteNumber(word.raw_start) || word.raw_start < 0
|
|
313
|
+
|| !isFiniteNumber(word.raw_end) || word.raw_end < word.raw_start)) {
|
|
314
|
+
fail(`${label} は 0 <= raw_start <= raw_end を満たす必要があります`);
|
|
315
|
+
}
|
|
309
316
|
if (!isNonEmptyString(word.text)) {
|
|
310
317
|
fail(`${label}.text は空でない文字列である必要があります`);
|
|
311
318
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// gen-models.json v1 のスキーマと、行をまたぐ意味制約を検証する。
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
9
|
+
|
|
10
|
+
const usage = "使い方: node packages/schemas/bin/validate-gen-models.mjs <gen-models.json>";
|
|
11
|
+
const catalogArgument = process.argv[2];
|
|
12
|
+
|
|
13
|
+
if (!catalogArgument || process.argv.length !== 3) {
|
|
14
|
+
console.error(usage);
|
|
15
|
+
process.exit(2);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (catalogArgument === "--help" || catalogArgument === "-h") {
|
|
19
|
+
console.log(usage);
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const catalogPath = path.resolve(catalogArgument);
|
|
24
|
+
const schemaPath = fileURLToPath(new URL("../gen-models.schema.json", import.meta.url));
|
|
25
|
+
const errors = [];
|
|
26
|
+
|
|
27
|
+
if (!isRegularFile(catalogPath)) {
|
|
28
|
+
fail(`gen-models.json が見つかりません: ${catalogPath}`);
|
|
29
|
+
finish();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let schema;
|
|
33
|
+
try {
|
|
34
|
+
schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
|
|
35
|
+
} catch (error) {
|
|
36
|
+
fail(`gen-models.schema.json を JSON として読めません: ${messageOf(error)}`);
|
|
37
|
+
finish();
|
|
38
|
+
}
|
|
39
|
+
if (schema.$id !== "urn:akari-video:schema:gen-models:v1") {
|
|
40
|
+
fail("gen-models.schema.json の $id が v1 契約と一致しません");
|
|
41
|
+
finish();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let catalog;
|
|
45
|
+
try {
|
|
46
|
+
catalog = JSON.parse(fs.readFileSync(catalogPath, "utf8"));
|
|
47
|
+
} catch (error) {
|
|
48
|
+
fail(`gen-models.json を JSON として読めません: ${messageOf(error)}`);
|
|
49
|
+
finish();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const validate = new Ajv2020({ allErrors: true, strict: true }).compile(schema);
|
|
54
|
+
if (!validate(catalog)) {
|
|
55
|
+
for (const error of validate.errors ?? []) {
|
|
56
|
+
fail(`スキーマ違反 ${error.instancePath || "/"}: ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
fail(`gen-models.schema.json をコンパイルできません: ${messageOf(error)}`);
|
|
61
|
+
finish();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
validateCatalog(catalog);
|
|
65
|
+
finish();
|
|
66
|
+
|
|
67
|
+
function validateCatalog(value) {
|
|
68
|
+
if (!value || typeof value !== "object" || !Array.isArray(value.models)) return;
|
|
69
|
+
if (value.models.length !== 14) fail(`models は 14 行である必要があります: ${value.models.length}`);
|
|
70
|
+
|
|
71
|
+
const ids = new Set();
|
|
72
|
+
for (const [index, model] of value.models.entries()) {
|
|
73
|
+
if (!model || typeof model !== "object" || Array.isArray(model)) continue;
|
|
74
|
+
const label = typeof model.id === "string" ? model.id : `models[${index}]`;
|
|
75
|
+
if (typeof model.id === "string") {
|
|
76
|
+
if (ids.has(model.id)) fail(`id が重複しています: ${model.id}`);
|
|
77
|
+
ids.add(model.id);
|
|
78
|
+
}
|
|
79
|
+
if (model.verified !== "documented") {
|
|
80
|
+
fail(`${label} の verified は documented である必要があります`);
|
|
81
|
+
}
|
|
82
|
+
if (model.price && typeof model.price === "object" && model.price.by_resolution) {
|
|
83
|
+
const resolutions = new Set(Array.isArray(model.resolutions) ? model.resolutions : []);
|
|
84
|
+
for (const resolution of Object.keys(model.price.by_resolution)) {
|
|
85
|
+
if (resolution !== "" && !resolutions.has(resolution)) {
|
|
86
|
+
fail(`${label} の price.by_resolution.${resolution} は resolutions に含まれていません`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
validateDuration(model.duration, label);
|
|
91
|
+
}
|
|
92
|
+
findForbiddenWord(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function validateDuration(duration, label) {
|
|
96
|
+
if (!duration || typeof duration !== "object") return;
|
|
97
|
+
if (duration.kind === "range") {
|
|
98
|
+
if (duration.min > duration.max) fail(`${label} の duration.min は max 以下である必要があります`);
|
|
99
|
+
if (duration.default !== null
|
|
100
|
+
&& (duration.default < duration.min || duration.default > duration.max
|
|
101
|
+
|| (duration.default - duration.min) % duration.step !== 0)) {
|
|
102
|
+
fail(`${label} の duration.default は range 内の step に一致する必要があります`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (duration.kind === "enum" && duration.default !== null && !duration.values.includes(duration.default)) {
|
|
106
|
+
fail(`${label} の duration.default は values に含まれている必要があります`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function findForbiddenWord(value, location = "ルート") {
|
|
111
|
+
if (typeof value === "string") {
|
|
112
|
+
if (value === "cap" + "ability") fail(`${location} に禁止語が値として使われています`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (!value || typeof value !== "object") return;
|
|
116
|
+
for (const [key, child] of Object.entries(value)) {
|
|
117
|
+
if (key === "cap" + "ability") fail(`${location} に禁止語のキーがあります`);
|
|
118
|
+
findForbiddenWord(child, `${location}.${key}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isRegularFile(targetPath) {
|
|
123
|
+
try {
|
|
124
|
+
return fs.statSync(targetPath).isFile();
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function messageOf(error) {
|
|
131
|
+
return error instanceof Error ? error.message : String(error);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function fail(message) {
|
|
135
|
+
errors.push(message);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function finish() {
|
|
139
|
+
if (errors.length > 0) {
|
|
140
|
+
for (const error of errors) console.error(`ERROR: ${error}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
console.log(`OK: ${catalogPath}`);
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// <元ファイル名>.meta.json の生成サイドカー v1 を検証する。
|
|
4
|
+
// 素材工房の asset-meta.schema.json とは別契約。
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
10
|
+
|
|
11
|
+
const usage = "使い方: node packages/schemas/bin/validate-generation-meta.mjs <元ファイル名.meta.json>";
|
|
12
|
+
const argument = process.argv[2];
|
|
13
|
+
|
|
14
|
+
if (!argument || process.argv.length !== 3) {
|
|
15
|
+
console.error(usage);
|
|
16
|
+
process.exit(2);
|
|
17
|
+
}
|
|
18
|
+
if (argument === "--help" || argument === "-h") {
|
|
19
|
+
console.log(usage);
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const metaPath = path.resolve(argument);
|
|
24
|
+
const schemaPath = fileURLToPath(new URL("../generation-meta.schema.json", import.meta.url));
|
|
25
|
+
let schema;
|
|
26
|
+
let meta;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error(`NG: ${metaPath}`);
|
|
32
|
+
console.error(`- generation-meta.schema.json を JSON として読めません: ${messageOf(error)}`);
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
if (schema.$id !== "urn:akari-video:schema:generation-meta:v1") {
|
|
36
|
+
console.error(`NG: ${metaPath}`);
|
|
37
|
+
console.error("- generation-meta.schema.json の $id が v1 契約と一致しません");
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
meta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(`NG: ${metaPath}`);
|
|
44
|
+
console.error(`- 生成サイドカーを JSON として読めません: ${messageOf(error)}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
49
|
+
ajv.addFormat("date-time", value => typeof value === "string" && Number.isFinite(Date.parse(value)) && /^\d{4}-\d{2}-\d{2}T/.test(value));
|
|
50
|
+
const validate = ajv.compile(schema);
|
|
51
|
+
if (!validate(meta)) {
|
|
52
|
+
console.error(`NG: ${metaPath}`);
|
|
53
|
+
for (const error of validate.errors ?? []) console.error(`- ${japaneseError(error)}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(`OK: ${metaPath}`);
|
|
58
|
+
|
|
59
|
+
function japaneseError(error) {
|
|
60
|
+
const location = error.instancePath || "/";
|
|
61
|
+
switch (error.keyword) {
|
|
62
|
+
case "required": return `${location} に必須キー ${error.params.missingProperty} がありません`;
|
|
63
|
+
case "additionalProperties": return `${location} に未定義キー ${error.params.additionalProperty} があります`;
|
|
64
|
+
case "enum": return `${location} の値が許可された候補と一致しません`;
|
|
65
|
+
case "const": return `${location} は ${JSON.stringify(error.params.allowedValue)} である必要があります`;
|
|
66
|
+
case "type": return `${location} は ${error.params.type} である必要があります`;
|
|
67
|
+
case "format": return `${location} は ${error.params.format} 形式である必要があります`;
|
|
68
|
+
case "pattern": return `${location} の文字列形式が契約と一致しません`;
|
|
69
|
+
default: return `${location} が契約を満たしません(${error.keyword})`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function messageOf(error) {
|
|
74
|
+
return error instanceof Error ? error.message : String(error);
|
|
75
|
+
}
|
|
@@ -247,9 +247,11 @@
|
|
|
247
247
|
"properties": {
|
|
248
248
|
"start": { "type": "number", "minimum": 0 },
|
|
249
249
|
"end": { "type": "number", "minimum": 0 },
|
|
250
|
+
"raw_start": { "type": "number", "minimum": 0 },
|
|
251
|
+
"raw_end": { "type": "number", "minimum": 0 },
|
|
250
252
|
"text": { "type": "string", "minLength": 1, "pattern": "\\S" }
|
|
251
253
|
},
|
|
252
|
-
"$comment": "end >= start と親 caption の範囲外警告は JSON Schema 標準だけでは兄弟値を比較できないため検証器が扱う。"
|
|
254
|
+
"$comment": "raw_start/raw_end は発話吸着前の監査用時刻。end >= start、raw_end >= raw_start と親 caption の範囲外警告は JSON Schema 標準だけでは兄弟値を比較できないため検証器が扱う。"
|
|
253
255
|
},
|
|
254
256
|
"sourceRef": {
|
|
255
257
|
"oneOf": [
|