akari-video 1.3.0-beta.9 → 1.3.0
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/history-policy.mjs +5 -0
- package/vendor/docs/contract-2026-10-12-caption-look-v1.md +44 -1
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/akari-launcher/src/history-policy.mjs +5 -0
- package/vendor/packages/edit-lint/src/lint/captions.mjs +9 -2
- package/vendor/packages/edit-lint/src/lint/external.mjs +1 -0
- package/vendor/packages/edit-store/lib/caption-hash.d.ts +2 -0
- package/vendor/packages/edit-store/lib/caption-hash.js +6 -1
- package/vendor/packages/edit-store/lib/caption-look-reader.d.ts +2 -0
- package/vendor/packages/edit-store/lib/caption-look-reader.js +93 -28
- package/vendor/packages/edit-store/lib/caption-look-shape.d.ts +18 -0
- package/vendor/packages/edit-store/lib/caption-look-shape.js +134 -0
- package/vendor/packages/edit-store/lib/caption-look-v1.d.ts +1 -0
- package/vendor/packages/edit-store/lib/caption-look-v1.js +2 -1
- package/vendor/packages/edit-store/lib/caption-store.d.ts +4 -0
- package/vendor/packages/edit-store/lib/caption-store.js +10 -2
- package/vendor/packages/edit-store/lib/caption-style-command.d.ts +127 -0
- package/vendor/packages/edit-store/lib/caption-style-command.js +422 -0
- package/vendor/packages/edit-store/lib/caption-style-write.d.ts +57 -0
- package/vendor/packages/edit-store/lib/caption-style-write.js +116 -0
- package/vendor/packages/edit-store/lib/index.d.ts +2 -0
- package/vendor/packages/edit-store/lib/index.js +2 -0
- package/vendor/packages/edit-store/lib/write-gate.d.ts +48 -2
- package/vendor/packages/edit-store/lib/write-gate.js +659 -25
- package/vendor/packages/i18n/locales/en/annotations.json +3 -0
- package/vendor/packages/i18n/locales/en/annotationscommon.json +9 -1
- package/vendor/packages/i18n/locales/en/tools.json +2 -0
- package/vendor/packages/i18n/locales/ja/annotations.json +3 -0
- package/vendor/packages/i18n/locales/ja/annotationscommon.json +9 -1
- package/vendor/packages/i18n/locales/ja/tools.json +2 -0
- package/vendor/packages/schemas/test/caption-look-shape-parity.test.mjs +73 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "1.3.0
|
|
3
|
+
"version": "1.3.0",
|
|
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/history-policy.mjs
CHANGED
|
@@ -60,6 +60,11 @@ const HISTORY_BLOCK_BODY = [
|
|
|
60
60
|
'.akari/cache/**',
|
|
61
61
|
'.akari/diffs/**',
|
|
62
62
|
'!.akari/diffs/.gitkeep',
|
|
63
|
+
'.akari/write.lock',
|
|
64
|
+
'.akari/write.lock.claim/',
|
|
65
|
+
'.akari/write.lock.wait',
|
|
66
|
+
'.akari/write-journal.json',
|
|
67
|
+
'.akari/work/journal/',
|
|
63
68
|
'',
|
|
64
69
|
'# 素材の分析結果は作り直すのに手間がかかるので、変更履歴に残します。',
|
|
65
70
|
'!.akari/sidecars/**',
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
## 移行と書き出し
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
移行は主経路(`resolveCaptionPlan` を通る書き出しとプレビュー)で、前後の HTML と CSS 変数を保つ。移行済みの強調範囲は、描画時に `migrated_emphasis` に保存した元の項目と移行前の配列内の位置から `emphasis_words` へ戻す。残った項目の順を保ち、移した項目を位置の昇順に差し込む。追加のプロパティと項目の順序も保つ。旧形式だけの入力はそのまま描画器へ渡す。
|
|
9
9
|
|
|
10
10
|
| `style_hint`(省略時の `emotion`) | `motion.in.id` |
|
|
11
11
|
|---|---|
|
|
@@ -52,3 +52,46 @@ source anchor は字幕が描かれる同じ素材の配置ごとに、速度と
|
|
|
52
52
|
## 検証
|
|
53
53
|
|
|
54
54
|
構造は captions schema が判定する。文書の意味では v1 run の範囲外、ファイル内の run id 重複、anchor のない `spoken` を error として保存を拒む。`validate-captions` も文書の意味の層を呼び、error があれば非 0 で終了する。旧 runs v0 の範囲外・空の区間は従来どおり `captions.run-range` の warning とする。意味の error は診断を出す。描画経路での除外は後の版で行う(除外に使う純関数 `renderableCaptionRuns` は用意済み)。プロジェクトの意味では anchor の写像不能や表示窓の外を warning とし、経路の対応では選んだ描画経路が motion を描けるか判定する。診断はコード・重大度・パス・日本語メッセージを共有する。
|
|
55
|
+
|
|
56
|
+
## 書き込みの境界
|
|
57
|
+
|
|
58
|
+
`writeProjectFilesTransactional(projectRoot, { files, label })` は、プロジェクト内の相対パスごとに `expectedHash` と `next` を受け取る。`expectedHash` は生のバイト列の SHA-256 を `sha256:<64 桁の十六進数>` で指定する。`null` はファイルが無いことを期待し、省略時は比較しない。`next: null` は比較だけを行う。読んだだけの `edit.json` なども比較へ含められる。1 件でも違えば何も書かず、`ConflictError` の `files` に競合した相対パスを返す。
|
|
59
|
+
|
|
60
|
+
書き込みは `.akari/write.lock` の排他作成でプロセス間を直列化する。lock の持ち主が同じホストで終了した場合、または作成から 30 秒を超えた場合は取得し直せる。同じプロセス内でもプロジェクトごとに順番を保つ。比較のない書き込みは lock の下でファイルごとに一時ファイルから rename し、途中で失敗しても先に書いたファイルを戻さず、journal と `.akari/work/` は作らない。比較つきの書き込みは、変更前に `.akari/write-journal.json` と `.akari/work/journal/<id>/` の元バイト列を保存する。途中で失敗したら元のバイト列に戻す。すべての rename が済んだら journal に `committed: true` を記録し、片付けに失敗しても保存は成功とする。
|
|
61
|
+
|
|
62
|
+
lock を待つ側は `.akari/write.lock.wait` に印を置く。放す側は lock を消した後でその印を消し、待ち手に短く取得を譲る。
|
|
63
|
+
|
|
64
|
+
書き込み先の相対パスは正規化し、同じファイルを指す鍵の重複、代替データストリームの `:`、lock・journal・退避先を拒む。`./edit.json` は `edit.json` として扱う。比較する hash は生バイトを基準にするが、UTF-8 BOM 付きのファイルでは BOM を除いたバイト列の hash も一致として受け入れる。元の BOM は取り消し用の変更前全文に残す。
|
|
65
|
+
|
|
66
|
+
比較がすべて通った後、実際に書く各ファイルの rename 直前に `onWillWrite(destination, name)` を呼び、rename 完了後に同じファイルの `onDidWrite` を呼ぶ。競合や内容が同じため書かないファイルにはどちらも通知しない。
|
|
67
|
+
|
|
68
|
+
次の書き込みの前に journal が残っていれば回復する。`committed: true` なら戻さず片付ける。印がなければ現在の内容が止まった書き込みの値と一致するファイルだけ元に戻し、元の値と一致するものはそのままにする。どちらでもないファイルや退避が読めないファイルは触らず通知する。journal が壊れていれば退避し、ファイルは触らない。回復通知は保存結果の `recovered` と `onRecovered` で受け取れる。journal が無ければ起動時の回復は lock を取らず直ちに終わる。
|
|
69
|
+
|
|
70
|
+
## 見た目の編集コマンド
|
|
71
|
+
|
|
72
|
+
`planCaptionStyleEdit` は `captions`、`targets`、`parts`、`source` を受け、副作用なしに `next`、`changed`、`notices` を返す。対象は `{caption_id}`(行)、`{caption_id, range}`(文字範囲)、`{default:true}`(既定スタイル)。対象ごとに `parts` を上書きできる。`range` は書記素の半開区間 `{from,to}`、または `{quote,occurrence}` で指定する。引用の候補は NFC にそろえ、左から重ならないように数える。一致後はその末尾から次を探す(「ああああ」の「ああ」は 2 件)。返す位置は元の文字列の書記素単位。同じ引用が複数あれば 1 始まりの `occurrence` が必要。引用が見つからない場合や範囲が空・範囲外の場合は失敗する。範囲の効果音と効果は使えず、行の効果音と効果は従来の attach 経路で扱う。
|
|
73
|
+
|
|
74
|
+
行の look は許可項目を部品ごとに置換し、動きは `animation` 全体を置換する。範囲は `runs[].look` と `runs[].motion` を置換し、許可外の look 項目を外して通知する。同じ区間の v1 run は更新し、新しい区間にはファイル全体で一意の id を付ける。実測 words に対応付けられる場合だけ anchor を付ける。行の旧 `style_preset` は現在のカタログで値に解き、出所を記録してから外す。カタログに無ければ元の指定を保って通知する。配列ルートに `{default:true}` を当てた場合は `{default_text_style,captions}` にする。
|
|
75
|
+
|
|
76
|
+
スタイルを当てても大きさは変えない。行の既存の `size_px` と `reference_height_px` を保ち、`size_px` が無ければ実効値を明示する。px 系の値は当て先の大きさに換算する。`style_sources.look` の hash は look の許可項目から `size_px` と `reference_height_px` を除いた値に付ける。`original_hash` は出所の値、`applied_hash` は換算後に保存した値である。`applied_at` には適用時刻を記録する。
|
|
77
|
+
|
|
78
|
+
`planCaptionStyleClear` は run の指定全体を消す `mode: "run"`、選んだ文字を重なる run から切り取る `mode: "range"`、範囲指定のない `{caption_id}` で行の look・動き・出所・旧 preset を消す操作を扱う。行の大きさと配置は保つ。`planCaptionStyleReapply` は出所の現在の値を引き、部品ごとに `same`、`updated`、`missing` と手直しの有無を返す。`apply` に指定した部品だけ値を当て直す。出所が無ければ値を保って通知する。
|
|
79
|
+
|
|
80
|
+
`planCaptionSizeEdit({captions,targets,size_px})` は行の大きさと、その行の look および範囲の look の px 系の値を、`新しい大きさ ÷ 変更前の実効の大きさ` で変える。小数は 6 桁に丸め、`reference_height_px` を保つ。比が 1 なら変更しない。元の `applied_hash` と値が一致していた部品は換算後の hash に更新し、大きさの変更だけで手直しとは判定しない。
|
|
81
|
+
|
|
82
|
+
Node 側の `applyCaptionStyle`、`clearCaptionStyle`、`reapplyCaptionStyle`、`setCaptionSize` は読んだ全文の hash を比較してから保存し、書く前と後の全文・hash を返す。対象ファイルは既定の `captions.json` / `edit.json` に加え、プロジェクト直下の対応する `captions.<slug>.json` / `edit.<slug>.json` を指定できる。期待 hash は役割名(`captions` / `edit`)または実ファイル名で受け、前後の全文と `written` は実ファイル名を鍵にする。読んだだけのファイルも競合の比較に含める。保存前に v1 の項目の形と文書の意味を検査し、error があれば `AKARI_CAPTIONS_INVALID` と診断を返して書かない。形の検査は旧い行の他の項目や v0 の run の形を理由に拒まない。edit-lint も同じ v1 の形の診断を表示する。
|
|
83
|
+
|
|
84
|
+
書き込み lock の奪取に使う `.akari/write.lock.claim` は、5 秒を超えて残ったものを取り除いてから再取得する。書き込み先のフォルダで作成権限が無いと確かめたときは、待ち上限を待たずに権限エラーを返す。
|
|
85
|
+
`.akari/write.lock.claim` とその下、および試し書き名 `.akari/write.lock.probe-*` は書き込み先に指定できない。
|
|
86
|
+
|
|
87
|
+
## 移行と CLI
|
|
88
|
+
|
|
89
|
+
移行と preset の解決には同梱とライブラリを合わせた現在の textstyle カタログを使う。移行は現在の edit.json も読んで計画し、書き込む前の字幕データを保存境界の期待 hash として扱う。出所を指定せず look または motion を当てた場合、その部品の古い `style_sources` を消す。両部品の出所が無くなれば `style_sources` 自体を消す。
|
|
90
|
+
|
|
91
|
+
CLI の `--caption <index>` は id の無い行や id が重複した行にも index で当てる。`--caption <id>` は id で指定する。CLI の JSON 出力の `changes[].before` と `changes[].after` は実際に計画または保存した文書の値を示す。
|
|
92
|
+
|
|
93
|
+
アプリの `applyCaptionStyle`、`clearCaptionStyle`、`reapplyCaptionStyle`、`setCaptionSize`、`migrateCaptionLook` は projectRootUri と対象 URI を検査し、`captionsUri` と対になる edit のあるフォルダを根として別名の組も扱い、任意の期待 hash を受ける。成功は実ファイル名で書いたファイル、通知、前後の全文と hash を返す。`writeEditSnapshot` の期待 hash も edit と captions の役割に対応する候補を比較する。競合は `conflict: true`、検証エラーは `invalid: true` と診断を返し、どちらも書かない。中断した書き込みは `recoverInterruptedWrite` で元に戻す。アプリはプロジェクトを開いたときに回復を試み、通知を表示する。字幕スタイルの undo と redo は前後の全文を各 1 件の履歴に持ち、保存後の hash と一致するときだけ逆向きに書く。競合したら読み直して取り消しややり直しを失敗として知らせる。
|
|
94
|
+
|
|
95
|
+
移行の保存はプロジェクト直下の `captions.json` と `captions.<slug>.json` を対象にし、対応する edit を比較する。強調語の id は移行済みと未解決の run に残る `migrated_from` も予約済みとして採番する。古い強調語と run の id が重なれば、強調語に新しい id を与えてから移す。主経路(`resolveCaptionPlan` を通る書き出しとプレビュー)では移行の前後で HTML と CSS 変数が同じ。取り出した字幕と GPU の適格判定の入口は未対応のため、移行の保存はまだ一般の利用に勧めない。
|
|
96
|
+
|
|
97
|
+
`akari-apply-textstyle` の既存の対象指定、`--dry-run`、JSON 出力と終了コードは保つ。保存には同じ書き込み境界を使う。行に当てた場合は `style_sources.look` を記録し、旧 `style_preset` を値に解いて外す。既存の CLI が行っていた深い合成は互換のため維持する。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "1.3.0
|
|
3
|
+
"version": "1.3.0",
|
|
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": [
|
|
@@ -60,6 +60,11 @@ const HISTORY_BLOCK_BODY = [
|
|
|
60
60
|
'.akari/cache/**',
|
|
61
61
|
'.akari/diffs/**',
|
|
62
62
|
'!.akari/diffs/.gitkeep',
|
|
63
|
+
'.akari/write.lock',
|
|
64
|
+
'.akari/write.lock.claim/',
|
|
65
|
+
'.akari/write.lock.wait',
|
|
66
|
+
'.akari/write-journal.json',
|
|
67
|
+
'.akari/work/journal/',
|
|
63
68
|
'',
|
|
64
69
|
'# 素材の分析結果は作り直すのに手間がかかるので、変更履歴に残します。',
|
|
65
70
|
'!.akari/sidecars/**',
|
|
@@ -3,7 +3,7 @@ import { lintText } from '../i18n.mjs';
|
|
|
3
3
|
import { buildMatcher, protectedTermsFrom } from "../../../word-book/src/index.mjs";
|
|
4
4
|
import { resolveWordBookSync, scanRecord } from "../../../word-book/src/index.mjs";
|
|
5
5
|
import { EPSILON, addFinding, captionFinding, isFiniteNumber, isNonEmptyString, isPositiveNumber, isRecord, relativePath } from "./shared.mjs";
|
|
6
|
-
import { resolveCaptionDisplay, validateCaptionsSemantics, CAPTION_RUN_FIELDS } from "./external.mjs";
|
|
6
|
+
import { resolveCaptionDisplay, validateCaptionsSemantics, validateCaptionLookV1Shape, CAPTION_RUN_FIELDS } from "./external.mjs";
|
|
7
7
|
import { validateCaptionAnimation, validateTextStyle } from "./caption-style.mjs";
|
|
8
8
|
import { captionLanguageDefaults, resolveContentLanguage } from "../../../content-language/src/index.mjs";
|
|
9
9
|
import { readContentLanguageSync } from "../../../content-language/src/node.mjs";
|
|
@@ -452,7 +452,14 @@ export function validateCaptions(captions, edit, analysis, findings, paths, cuts
|
|
|
452
452
|
captionFinding(findings, "captions.display-policy", error instanceof Error ? error.message : String(error), captionPath);
|
|
453
453
|
}
|
|
454
454
|
}
|
|
455
|
-
|
|
455
|
+
const seenDiagnostics = new Set();
|
|
456
|
+
for (const diagnostic of [...validateCaptionLookV1Shape(captionsRoot),
|
|
457
|
+
...validateCaptionsSemantics({ captions: captionsRoot, edit, timeline })]) {
|
|
458
|
+
const identity = JSON.stringify([diagnostic.code, diagnostic.path, diagnostic.message]);
|
|
459
|
+
if (seenDiagnostics.has(identity)) continue;
|
|
460
|
+
if (diagnostic.code === 'captions.look.shape' && findings.some(item => item.severity === 'error'
|
|
461
|
+
&& item.check === 'captions.schema' && item.path === diagnostic.path)) continue;
|
|
462
|
+
seenDiagnostics.add(identity);
|
|
456
463
|
addFinding(findings, { severity: diagnostic.severity, check: diagnostic.code,
|
|
457
464
|
message: diagnostic.message, path: diagnostic.path });
|
|
458
465
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.captionValueHash = captionValueHash;
|
|
4
|
+
exports.projectFileContentHash = projectFileContentHash;
|
|
4
5
|
/** 鍵を再帰的に整列した JSON を UTF-8 で SHA-256 にする。 */
|
|
5
6
|
function captionValueHash(value) {
|
|
6
7
|
const sorted = (entry) => {
|
|
@@ -10,7 +11,11 @@ function captionValueHash(value) {
|
|
|
10
11
|
return Object.fromEntries(Object.keys(entry).sort().map(key => [key, sorted(entry[key])]));
|
|
11
12
|
return entry;
|
|
12
13
|
};
|
|
13
|
-
|
|
14
|
+
return projectFileContentHash(JSON.stringify(sorted(value)));
|
|
15
|
+
}
|
|
16
|
+
/** UTF-8 の生バイト列をブラウザでも同じ SHA-256 にする。 */
|
|
17
|
+
function projectFileContentHash(text) {
|
|
18
|
+
const bytes = new TextEncoder().encode(text);
|
|
14
19
|
const length = Math.ceil((bytes.length + 9) / 64) * 64;
|
|
15
20
|
const padded = new Uint8Array(length);
|
|
16
21
|
padded.set(bytes);
|
|
@@ -26,6 +26,8 @@ type CaptionsRoot = {
|
|
|
26
26
|
emphasis_words?: Emphasis[];
|
|
27
27
|
[key: string]: unknown;
|
|
28
28
|
};
|
|
29
|
+
/** 旧い強調の ID は移行済み run に残った出所 ID も予約済みとして扱う。 */
|
|
30
|
+
export declare function nextEmphasisWordId(root: CaptionsRoot | CaptionRow[]): string;
|
|
29
31
|
export declare const CAPTION_EMPHASIS_MOTION: Readonly<Record<string, string>>;
|
|
30
32
|
export declare function readCaptionLookV1(root: CaptionsRoot | CaptionRow[], catalog: TextstyleCatalog): {
|
|
31
33
|
captions: CaptionRow[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CAPTION_EMPHASIS_MOTION = void 0;
|
|
4
|
+
exports.nextEmphasisWordId = nextEmphasisWordId;
|
|
4
5
|
exports.readCaptionLookV1 = readCaptionLookV1;
|
|
5
6
|
exports.planCaptionLookMigration = planCaptionLookMigration;
|
|
6
7
|
exports.captionRenderInputFromV1 = captionRenderInputFromV1;
|
|
@@ -8,6 +9,30 @@ const caption_runs_1 = require("./caption-runs");
|
|
|
8
9
|
const caption_hash_1 = require("./caption-hash");
|
|
9
10
|
const caption_look_v1_1 = require("./caption-look-v1");
|
|
10
11
|
const caption_style_preset_1 = require("./caption-style-preset");
|
|
12
|
+
const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
13
|
+
const captionRows = (root) => Array.isArray(root) ? root : Array.isArray(root?.captions) ? root.captions : [];
|
|
14
|
+
const emphasisWords = (root) => Array.isArray(root) || !Array.isArray(root?.emphasis_words) ? [] : root.emphasis_words;
|
|
15
|
+
const unresolvedRuns = (root) => Array.isArray(root) || !Array.isArray(root?.unresolved_runs) ? []
|
|
16
|
+
: root.unresolved_runs.flatMap(item => isRecord(item) && isRecord(item.run)
|
|
17
|
+
? [item.run] : []);
|
|
18
|
+
/** 旧い強調の ID は移行済み run に残った出所 ID も予約済みとして扱う。 */
|
|
19
|
+
function nextEmphasisWordId(root) {
|
|
20
|
+
const used = new Set(emphasisWords(root).flatMap(item => isRecord(item) && typeof item.id === 'string'
|
|
21
|
+
? [item.id] : []));
|
|
22
|
+
for (const row of captionRows(root))
|
|
23
|
+
for (const run of rowRuns(row))
|
|
24
|
+
if (isRecord(run) && typeof run.migrated_from === 'string')
|
|
25
|
+
used.add(run.migrated_from);
|
|
26
|
+
for (const run of unresolvedRuns(root))
|
|
27
|
+
if (typeof run.migrated_from === 'string')
|
|
28
|
+
used.add(run.migrated_from);
|
|
29
|
+
let number = 1;
|
|
30
|
+
while (used.has(`e-${String(number).padStart(4, '0')}`))
|
|
31
|
+
number++;
|
|
32
|
+
if (number > 9999)
|
|
33
|
+
throw new Error('強調語の id に空きがありません。');
|
|
34
|
+
return `e-${String(number).padStart(4, '0')}`;
|
|
35
|
+
}
|
|
11
36
|
exports.CAPTION_EMPHASIS_MOTION = {
|
|
12
37
|
'one-char-bang': 'pop', 'one-char-jumble': 'jitter', 'size-pulse': 'zoom-pulse'
|
|
13
38
|
};
|
|
@@ -41,6 +66,8 @@ function projectEmphasis(rows, item) {
|
|
|
41
66
|
if (!motion)
|
|
42
67
|
return undefined;
|
|
43
68
|
const candidates = rows.flatMap(row => {
|
|
69
|
+
if (!isRecord(row))
|
|
70
|
+
return [];
|
|
44
71
|
if (row.runs !== undefined && (!Array.isArray(row.runs)
|
|
45
72
|
|| row.runs.some(run => !run || typeof run !== 'object' || Array.isArray(run))))
|
|
46
73
|
return [];
|
|
@@ -50,7 +77,7 @@ function projectEmphasis(rows, item) {
|
|
|
50
77
|
return candidates.length === 1 ? candidates[0] : undefined;
|
|
51
78
|
}
|
|
52
79
|
function rowRuns(row) {
|
|
53
|
-
return Array.isArray(row.runs) ? row.runs : [];
|
|
80
|
+
return isRecord(row) && Array.isArray(row.runs) ? row.runs : [];
|
|
54
81
|
}
|
|
55
82
|
function invalidLegacyRun(run, row) {
|
|
56
83
|
if (['id', 'look', 'motion', 'anchor', 'fire'].some(key => Object.prototype.hasOwnProperty.call(run, key))) {
|
|
@@ -61,15 +88,16 @@ function invalidLegacyRun(run, row) {
|
|
|
61
88
|
|| run.from < 0 || run.to > length || run.from >= run.to;
|
|
62
89
|
}
|
|
63
90
|
function readCaptionLookV1(root, catalog) {
|
|
64
|
-
const rows =
|
|
65
|
-
const emphasis =
|
|
91
|
+
const rows = captionRows(root);
|
|
92
|
+
const emphasis = emphasisWords(root);
|
|
66
93
|
const projected = new Map();
|
|
67
|
-
const allRuns = [...rows.flatMap(rowRuns), ...(
|
|
68
|
-
(Array.isArray(root.unresolved_runs) ? root.unresolved_runs.flatMap(item => item.run ? [item.run] : []) : []))];
|
|
94
|
+
const allRuns = [...rows.flatMap(rowRuns), ...unresolvedRuns(root)];
|
|
69
95
|
const occupiedIndices = new Set(allRuns.flatMap(run => run && typeof run === 'object'
|
|
70
96
|
&& Number.isInteger(run.migrated_emphasis?.index) ? [run.migrated_emphasis.index] : []));
|
|
71
97
|
let originalIndex = 0;
|
|
72
98
|
for (const item of emphasis) {
|
|
99
|
+
if (!isRecord(item))
|
|
100
|
+
continue;
|
|
73
101
|
while (occupiedIndices.has(originalIndex))
|
|
74
102
|
originalIndex++;
|
|
75
103
|
const index = originalIndex++;
|
|
@@ -80,12 +108,16 @@ function readCaptionLookV1(root, catalog) {
|
|
|
80
108
|
}
|
|
81
109
|
const notices = [];
|
|
82
110
|
const captions = rows.map(row => {
|
|
111
|
+
if (!isRecord(row))
|
|
112
|
+
return row;
|
|
83
113
|
const resolved = (0, caption_style_preset_1.resolveCaptionStylePreset)(row, catalog);
|
|
84
114
|
const runs = rowRuns(row).map(run => !run || typeof run !== 'object' || Array.isArray(run)
|
|
85
115
|
|| invalidLegacyRun(run, row) ? run : { ...run,
|
|
86
116
|
...(run.style && !run.look ? { look: { ...run.style } } : {}),
|
|
87
117
|
...(run.animation && !run.dormant_animation ? { dormant_animation: run.animation } : {}) });
|
|
88
118
|
for (const item of emphasis) {
|
|
119
|
+
if (!isRecord(item))
|
|
120
|
+
continue;
|
|
89
121
|
if (runs.some(run => run && typeof run === 'object' && run.migrated_from === item.id))
|
|
90
122
|
continue;
|
|
91
123
|
const interval = projected.get(item.id);
|
|
@@ -109,15 +141,15 @@ function readCaptionLookV1(root, catalog) {
|
|
|
109
141
|
}
|
|
110
142
|
function planCaptionLookMigration(captions, catalog, _edit) {
|
|
111
143
|
const source = structuredClone(captions);
|
|
112
|
-
const rows =
|
|
113
|
-
const emphasis =
|
|
144
|
+
const rows = captionRows(source);
|
|
145
|
+
const emphasis = emphasisWords(source);
|
|
114
146
|
const unresolved = [];
|
|
115
147
|
const notices = [];
|
|
116
|
-
const allRuns = [...rows.flatMap(rowRuns),
|
|
117
|
-
...(Array.isArray(source) ? [] : (Array.isArray(source.unresolved_runs)
|
|
118
|
-
? source.unresolved_runs.flatMap(item => item.run ? [item.run] : []) : []))];
|
|
148
|
+
const allRuns = [...rows.flatMap(rowRuns), ...unresolvedRuns(source)];
|
|
119
149
|
let invalidLegacyCount = 0;
|
|
120
150
|
for (const row of rows) {
|
|
151
|
+
if (!isRecord(row))
|
|
152
|
+
continue;
|
|
121
153
|
if (row.style_preset) {
|
|
122
154
|
const preset = (0, caption_style_preset_1.resolveCaptionStylePreset)(row, catalog);
|
|
123
155
|
if (preset.resolved) {
|
|
@@ -174,13 +206,15 @@ function planCaptionLookMigration(captions, catalog, _edit) {
|
|
|
174
206
|
const migrated = new Set();
|
|
175
207
|
let originalIndex = 0;
|
|
176
208
|
for (const item of emphasis) {
|
|
209
|
+
if (!isRecord(item))
|
|
210
|
+
continue;
|
|
177
211
|
while (occupiedIndices.has(originalIndex))
|
|
178
212
|
originalIndex++;
|
|
179
213
|
const index = originalIndex++;
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
214
|
+
if (allRuns.some(run => run && typeof run === 'object' && run.migrated_from === item.id)) {
|
|
215
|
+
const old = item.id;
|
|
216
|
+
item.id = nextEmphasisWordId(source);
|
|
217
|
+
notices.push(`強調語 ${old} は id が重なっていたので ${item.id} に振り直しました`);
|
|
184
218
|
}
|
|
185
219
|
const candidate = projectEmphasis(rows, item);
|
|
186
220
|
if (!candidate) {
|
|
@@ -199,13 +233,15 @@ function planCaptionLookMigration(captions, catalog, _edit) {
|
|
|
199
233
|
migrated.add(item.id);
|
|
200
234
|
occupiedIndices.add(index);
|
|
201
235
|
}
|
|
202
|
-
if (!Array.isArray(source) &&
|
|
203
|
-
source.emphasis_words = emphasis.filter(item => !migrated.has(item.id));
|
|
236
|
+
if (!Array.isArray(source) && Array.isArray(source.emphasis_words)) {
|
|
237
|
+
source.emphasis_words = emphasis.filter(item => !isRecord(item) || !migrated.has(item.id));
|
|
204
238
|
}
|
|
205
|
-
const originalRows =
|
|
239
|
+
const originalRows = captionRows(captions);
|
|
206
240
|
const diff = [];
|
|
207
241
|
rows.forEach((row, index) => {
|
|
208
|
-
|
|
242
|
+
if (!isRecord(row))
|
|
243
|
+
return;
|
|
244
|
+
const before = isRecord(originalRows[index]) ? originalRows[index] : {};
|
|
209
245
|
for (const key of new Set([...Object.keys(before), ...Object.keys(row)])) {
|
|
210
246
|
if (JSON.stringify(before[key]) !== JSON.stringify(row[key]))
|
|
211
247
|
diff.push({
|
|
@@ -218,19 +254,36 @@ function planCaptionLookMigration(captions, catalog, _edit) {
|
|
|
218
254
|
diff.push({ path: 'emphasis_words', before: captions.emphasis_words, after: source.emphasis_words });
|
|
219
255
|
}
|
|
220
256
|
return { next: source, diff,
|
|
221
|
-
unresolved, notices };
|
|
257
|
+
unresolved, notices: [...new Set(notices)] };
|
|
222
258
|
}
|
|
223
259
|
/** Restore the renderer's legacy input without changing a legacy-only document. */
|
|
224
260
|
function captionRenderInputFromV1(root) {
|
|
225
|
-
const rows =
|
|
261
|
+
const rows = captionRows(root);
|
|
262
|
+
const legacyIds = emphasisWords(root).flatMap(item => isRecord(item) ? [item.id] : []);
|
|
226
263
|
if (!rows.some(row => rowRuns(row).some(run => run && typeof run === 'object'
|
|
227
|
-
&& (run.look || run.dormant_animation || run.migrated_from)))
|
|
264
|
+
&& (run.look || run.dormant_animation || run.migrated_from)))
|
|
265
|
+
&& new Set(legacyIds).size === legacyIds.length)
|
|
228
266
|
return root;
|
|
229
267
|
const copy = structuredClone(root);
|
|
230
|
-
const nextRows =
|
|
231
|
-
const emphasis =
|
|
268
|
+
const nextRows = captionRows(copy);
|
|
269
|
+
const emphasis = [...emphasisWords(copy)];
|
|
270
|
+
const migratedIds = new Set();
|
|
271
|
+
for (const row of nextRows)
|
|
272
|
+
for (const run of rowRuns(row))
|
|
273
|
+
if (typeof run?.migrated_from === 'string')
|
|
274
|
+
migratedIds.add(run.migrated_from);
|
|
275
|
+
const ids = new Set();
|
|
276
|
+
for (const item of emphasis) {
|
|
277
|
+
if (!isRecord(item))
|
|
278
|
+
continue;
|
|
279
|
+
if (ids.has(item.id) || migratedIds.has(item.id))
|
|
280
|
+
item.id = nextEmphasisWordId({ captions: nextRows, emphasis_words: emphasis });
|
|
281
|
+
ids.add(item.id);
|
|
282
|
+
}
|
|
232
283
|
const migrated = [];
|
|
233
284
|
for (const row of nextRows) {
|
|
285
|
+
if (!isRecord(row))
|
|
286
|
+
continue;
|
|
234
287
|
if (!Array.isArray(row.runs))
|
|
235
288
|
continue;
|
|
236
289
|
row.runs = row.runs.flatMap((run) => {
|
|
@@ -240,13 +293,25 @@ function captionRenderInputFromV1(root) {
|
|
|
240
293
|
const hint = Object.keys(exports.CAPTION_EMPHASIS_MOTION)
|
|
241
294
|
.find(key => exports.CAPTION_EMPHASIS_MOTION[key] === run.motion?.in?.id);
|
|
242
295
|
const restored = run.migrated_emphasis?.item;
|
|
243
|
-
if (restored)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
296
|
+
if (restored) {
|
|
297
|
+
const item = structuredClone(restored);
|
|
298
|
+
if (ids.has(item.id))
|
|
299
|
+
item.id = nextEmphasisWordId({ captions: nextRows,
|
|
300
|
+
emphasis_words: [...emphasis, ...migrated.map(entry => entry.item)] });
|
|
301
|
+
ids.add(item.id);
|
|
302
|
+
migrated.push({ index: run.migrated_emphasis.index, item });
|
|
303
|
+
}
|
|
304
|
+
else if (hint) {
|
|
305
|
+
let id = run.migrated_from;
|
|
306
|
+
if (ids.has(id))
|
|
307
|
+
id = nextEmphasisWordId({ captions: nextRows,
|
|
308
|
+
emphasis_words: [...emphasis, ...migrated.map(entry => entry.item)] });
|
|
309
|
+
ids.add(id);
|
|
310
|
+
emphasis.push({ id,
|
|
247
311
|
word: (0, caption_runs_1.captionGraphemes)(row.display_text ?? row.text ?? '').slice(run.from, run.to).join(''),
|
|
248
312
|
t_start: run.anchor.start, t_end: run.anchor.end, emotion: run.emotion ?? 'emphasis',
|
|
249
313
|
style_hint: hint, ...(run.anchor.src ? { src: run.anchor.src } : {}) });
|
|
314
|
+
}
|
|
250
315
|
return [];
|
|
251
316
|
}
|
|
252
317
|
const next = { ...run };
|
|
@@ -263,7 +328,7 @@ function captionRenderInputFromV1(root) {
|
|
|
263
328
|
return [next];
|
|
264
329
|
});
|
|
265
330
|
}
|
|
266
|
-
if (!Array.isArray(copy) && (
|
|
331
|
+
if (!Array.isArray(copy) && (Array.isArray(copy.emphasis_words) || migrated.length)) {
|
|
267
332
|
const restored = [...emphasis];
|
|
268
333
|
for (const entry of migrated.sort((left, right) => left.index - right.index)) {
|
|
269
334
|
restored.splice(Math.min(entry.index, restored.length), 0, entry.item);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type CaptionSemanticDiagnostic, type CaptionSemanticsContext } from './caption-semantics';
|
|
2
|
+
export declare const CAPTION_LOOK_SHAPE_ANIMATION_IDS: string[];
|
|
3
|
+
export declare const CAPTION_LOOK_SHAPE_KEYS: {
|
|
4
|
+
run: string[];
|
|
5
|
+
look: string[];
|
|
6
|
+
anchor: string[];
|
|
7
|
+
styleSources: string[];
|
|
8
|
+
source: string[];
|
|
9
|
+
unresolvedRun: string[];
|
|
10
|
+
};
|
|
11
|
+
/** v1 の追加項目だけを検査する。旧い行の他の項目と runs v0 の形は許す。 */
|
|
12
|
+
export declare function validateCaptionLookV1Shape(captionsRoot: unknown): CaptionSemanticDiagnostic[];
|
|
13
|
+
export declare class CaptionsInvalidError extends Error {
|
|
14
|
+
readonly diagnostics: CaptionSemanticDiagnostic[];
|
|
15
|
+
readonly code = "AKARI_CAPTIONS_INVALID";
|
|
16
|
+
constructor(diagnostics: CaptionSemanticDiagnostic[]);
|
|
17
|
+
}
|
|
18
|
+
export declare function assertCaptionsWritable(captionsRoot: CaptionSemanticsContext['captions'], ctx?: Omit<CaptionSemanticsContext, 'captions'>): void;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CaptionsInvalidError = exports.CAPTION_LOOK_SHAPE_KEYS = exports.CAPTION_LOOK_SHAPE_ANIMATION_IDS = void 0;
|
|
4
|
+
exports.validateCaptionLookV1Shape = validateCaptionLookV1Shape;
|
|
5
|
+
exports.assertCaptionsWritable = assertCaptionsWritable;
|
|
6
|
+
const caption_semantics_1 = require("./caption-semantics");
|
|
7
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
const has = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
9
|
+
const number = (value) => typeof value === 'number' && Number.isFinite(value);
|
|
10
|
+
const positive = (value) => number(value) && value > 0;
|
|
11
|
+
const nonnegative = (value) => number(value) && value >= 0;
|
|
12
|
+
const integer = (value) => Number.isInteger(value);
|
|
13
|
+
const nonempty = (value) => typeof value === 'string' && value.length > 0;
|
|
14
|
+
const hex = (value) => typeof value === 'string' && /^#(?:[\da-fA-F]{3}|[\da-fA-F]{6}|[\da-fA-F]{8})$/.test(value);
|
|
15
|
+
const runId = (value) => typeof value === 'string' && /^r-\d{4,}$/.test(value);
|
|
16
|
+
const fire = (value) => ['shown', 'spoken', 'whole'].includes(String(value));
|
|
17
|
+
const enumOf = (...values) => (value) => typeof value === 'string' && values.includes(value);
|
|
18
|
+
function objectRule(fields, required = [], minProperties = 0) {
|
|
19
|
+
return value => isObject(value) && Object.keys(value).length >= minProperties
|
|
20
|
+
&& required.every(key => has(value, key))
|
|
21
|
+
&& Object.entries(value).every(([key, entry]) => fields[key] !== undefined && fields[key](entry));
|
|
22
|
+
}
|
|
23
|
+
const stroke = objectRule({ method: enumOf('webkit-outline'), color: hex, width_px: nonnegative });
|
|
24
|
+
const shadow = objectRule({ color: hex, opacity: value => number(value) && value >= 0 && value <= 1,
|
|
25
|
+
blur_px: nonnegative, distance_px: nonnegative, angle_deg: number }, ['color']);
|
|
26
|
+
const glow = objectRule({ color: hex, density: nonnegative, spread: nonnegative, offset_x: number, offset_y: number }, ['color']);
|
|
27
|
+
const background = objectRule({ color: hex, opacity: value => number(value) && value >= 0 && value <= 1,
|
|
28
|
+
radius_px: nonnegative, padding_px: nonnegative, width_pct: nonnegative, height_pct: nonnegative,
|
|
29
|
+
fit: enumOf('text', 'frame'), offset_x: number, offset_y: number, mode: enumOf('per-line', 'block') });
|
|
30
|
+
const lookFields = { color: hex, font_weight: value => integer(value) && value >= 1 && value <= 1000,
|
|
31
|
+
italic: value => typeof value === 'boolean', underline: value => typeof value === 'boolean', letter_spacing_em: number,
|
|
32
|
+
scale: positive, baseline_shift_em: number, rotate_deg: value => number(value) && Math.abs(value) <= 180,
|
|
33
|
+
stroke, shadow, glow, background };
|
|
34
|
+
const look = objectRule(lookFields);
|
|
35
|
+
exports.CAPTION_LOOK_SHAPE_ANIMATION_IDS = ['fade-in-out', 'soft-fade', 'fade-up', 'fade-down', 'cinematic-fade',
|
|
36
|
+
'slide-left', 'slide-right', 'slide-up', 'slide-down', 'push-left', 'push-right', 'push-up', 'push-down',
|
|
37
|
+
'rise-soft', 'drop-in', 'zoom-in-out', 'zoom-pop', 'zoom-pulse', 'pop', 'bounce', 'squash-pop',
|
|
38
|
+
'stretch-in', 'stomp', 'snap', 'rotate-in', 'spin-in', 'roll-in', 'spiral-in', 'swing', 'shake',
|
|
39
|
+
'jitter', 'glitch', 'flash', 'heartbeat', 'typewriter', 'wipe-left', 'wipe-right', 'wobble',
|
|
40
|
+
'float', 'breath', 'neon-flicker', 'hologram', 'retro-flicker', 'caption-rise', 'news-ticker',
|
|
41
|
+
'marquee-left', 'crawl-up'];
|
|
42
|
+
const animationIds = new Set(exports.CAPTION_LOOK_SHAPE_ANIMATION_IDS);
|
|
43
|
+
const slot = objectRule({ id: value => typeof value === 'string' && animationIds.has(value), duration_sec: positive,
|
|
44
|
+
ease: value => value === null || typeof value === 'string' && /\S/.test(value),
|
|
45
|
+
amp: value => value === null || positive(value) }, ['id']);
|
|
46
|
+
const motion = objectRule({ in: slot, loop: slot, out: slot }, [], 1);
|
|
47
|
+
const anchorFields = { src: nonempty, start: nonnegative, end: nonnegative,
|
|
48
|
+
domain: enumOf('source', 'output') };
|
|
49
|
+
const anchor = objectRule(anchorFields, ['start', 'end', 'domain']);
|
|
50
|
+
const sourceFields = { kind: enumOf('builtin', 'library', 'my'), id: nonempty, uid: nonempty,
|
|
51
|
+
revision: value => integer(value) && value >= 0, original_hash: nonempty,
|
|
52
|
+
applied_hash: nonempty, applied_at: nonempty,
|
|
53
|
+
requires: value => Array.isArray(value) && value.every(objectRule({ id: nonempty,
|
|
54
|
+
version: value => typeof value === 'string', hash: nonempty }, ['id', 'version', 'hash'])) };
|
|
55
|
+
const source = objectRule(sourceFields, ['kind', 'id', 'original_hash', 'applied_hash']);
|
|
56
|
+
const styleSourcesFields = { look: source, motion: source };
|
|
57
|
+
const styleSources = objectRule(styleSourcesFields);
|
|
58
|
+
const migratedEmphasis = objectRule({ index: nonnegative, item: value => isObject(value) }, ['index', 'item']);
|
|
59
|
+
const v1Fields = {
|
|
60
|
+
id: runId, from: integer, to: integer, role: nonempty, look, motion, emotion: nonempty, anchor,
|
|
61
|
+
fire, fire_was: fire, style_sources: styleSources, needs_review: value => typeof value === 'boolean',
|
|
62
|
+
split_from: runId, migrated_from: value => typeof value === 'string' && /^e-\d{4,}$/.test(value),
|
|
63
|
+
dormant_animation: motion, migrated_emphasis: migratedEmphasis
|
|
64
|
+
};
|
|
65
|
+
const unresolvedFields = ['run', 'old_caption_id', 'old_text', 'old_src', 'old_start', 'old_end', 'reason'];
|
|
66
|
+
exports.CAPTION_LOOK_SHAPE_KEYS = {
|
|
67
|
+
run: [...Object.keys(v1Fields), 'style', 'animation'], look: Object.keys(lookFields), anchor: Object.keys(anchorFields),
|
|
68
|
+
styleSources: Object.keys(styleSourcesFields), source: Object.keys(sourceFields), unresolvedRun: unresolvedFields
|
|
69
|
+
};
|
|
70
|
+
const idRequired = ['look', 'motion', 'anchor', 'fire', 'style_sources', 'migrated_from'];
|
|
71
|
+
/** v1 の追加項目だけを検査する。旧い行の他の項目と runs v0 の形は許す。 */
|
|
72
|
+
function validateCaptionLookV1Shape(captionsRoot) {
|
|
73
|
+
const diagnostics = [];
|
|
74
|
+
const add = (path, field) => diagnostics.push({ code: 'captions.look.shape', severity: 'error',
|
|
75
|
+
path, message: `${field} の形が字幕の規則に合いません。`, layer: 'document' });
|
|
76
|
+
const rows = Array.isArray(captionsRoot) ? captionsRoot : isObject(captionsRoot) ? captionsRoot.captions : undefined;
|
|
77
|
+
const checkRun = (run, path) => {
|
|
78
|
+
if (!isObject(run)) {
|
|
79
|
+
add(path, 'run');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const v1 = Object.keys(v1Fields).some(key => has(run, key));
|
|
83
|
+
if (v1)
|
|
84
|
+
for (const key of ['from', 'to'])
|
|
85
|
+
if (!has(run, key))
|
|
86
|
+
add(path, key);
|
|
87
|
+
if (idRequired.some(key => has(run, key)) && !has(run, 'id'))
|
|
88
|
+
add(path, 'id');
|
|
89
|
+
for (const [key, rule] of Object.entries(v1Fields))
|
|
90
|
+
if (has(run, key) && !rule(run[key]))
|
|
91
|
+
add(`${path}.${key}`, key);
|
|
92
|
+
for (const key of Object.keys(run))
|
|
93
|
+
if (!has(v1Fields, key) && !['style', 'animation'].includes(key))
|
|
94
|
+
add(`${path}.${key}`, key);
|
|
95
|
+
};
|
|
96
|
+
if (Array.isArray(rows))
|
|
97
|
+
rows.forEach((row, index) => {
|
|
98
|
+
if (!isObject(row))
|
|
99
|
+
return;
|
|
100
|
+
if (has(row, 'style_sources') && !styleSources(row.style_sources))
|
|
101
|
+
add(`captions.json#captions[${index}].style_sources`, 'style_sources');
|
|
102
|
+
if (Array.isArray(row.runs))
|
|
103
|
+
row.runs.forEach((run, runIndex) => checkRun(run, `captions.json#captions[${index}].runs[${runIndex}]`));
|
|
104
|
+
});
|
|
105
|
+
if (isObject(captionsRoot) && has(captionsRoot, 'unresolved_runs')) {
|
|
106
|
+
const value = captionsRoot.unresolved_runs;
|
|
107
|
+
const valid = Array.isArray(value) && value.every((item, index) => {
|
|
108
|
+
if (!isObject(item))
|
|
109
|
+
return false;
|
|
110
|
+
checkRun(item.run, `captions.json#unresolved_runs[${index}].run`);
|
|
111
|
+
return unresolvedFields.every(key => has(item, key)) && Object.keys(item).every(key => unresolvedFields.includes(key))
|
|
112
|
+
&& isObject(item.run) && typeof item.old_caption_id === 'string'
|
|
113
|
+
&& typeof item.old_text === 'string' && (item.old_src === null || typeof item.old_src === 'string')
|
|
114
|
+
&& number(item.old_start) && number(item.old_end) && nonempty(item.reason);
|
|
115
|
+
});
|
|
116
|
+
if (!valid)
|
|
117
|
+
add('captions.json#unresolved_runs', 'unresolved_runs');
|
|
118
|
+
}
|
|
119
|
+
return diagnostics;
|
|
120
|
+
}
|
|
121
|
+
class CaptionsInvalidError extends Error {
|
|
122
|
+
constructor(diagnostics) {
|
|
123
|
+
super(`字幕の内容を保存できません: ${diagnostics.map(item => item.message).join('、')}`);
|
|
124
|
+
this.diagnostics = diagnostics;
|
|
125
|
+
this.code = 'AKARI_CAPTIONS_INVALID';
|
|
126
|
+
this.name = 'CaptionsInvalidError';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
exports.CaptionsInvalidError = CaptionsInvalidError;
|
|
130
|
+
function assertCaptionsWritable(captionsRoot, ctx = {}) {
|
|
131
|
+
const diagnostics = [...validateCaptionLookV1Shape(captionsRoot), ...(0, caption_semantics_1.validateCaptionsSemantics)({ ...ctx, captions: captionsRoot })];
|
|
132
|
+
if (diagnostics.some(item => item.severity === 'error'))
|
|
133
|
+
throw new CaptionsInvalidError(diagnostics);
|
|
134
|
+
}
|
|
@@ -27,6 +27,7 @@ export declare function lookForCaptionRow(row: Record<string, unknown>, defaultS
|
|
|
27
27
|
text_style?: Record<string, unknown>;
|
|
28
28
|
notices: string[];
|
|
29
29
|
reason?: string;
|
|
30
|
+
reasonCode?: 'AKARI_CAPTION_LAYOUT_REFERENCE_CONFLICT';
|
|
30
31
|
};
|
|
31
32
|
export declare function lookForCaptionRun(row: Record<string, unknown>, defaultStyle: Record<string, unknown>, styleLook: Record<string, unknown>, catalog?: Record<string, {
|
|
32
33
|
style: Record<string, unknown>;
|
|
@@ -146,7 +146,8 @@ function lookForCaptionRow(row, defaultStyle, styleLook, options) {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
if (textStyle.layout !== undefined && textStyle.reference_height_px !== undefined) {
|
|
149
|
-
return { notices: [], reason: 'layout と reference_height_px を同時に指定できません'
|
|
149
|
+
return { notices: [], reason: 'layout と reference_height_px を同時に指定できません',
|
|
150
|
+
reasonCode: 'AKARI_CAPTION_LAYOUT_REFERENCE_CONFLICT' };
|
|
150
151
|
}
|
|
151
152
|
return { text_style: textStyle, notices: [] };
|
|
152
153
|
}
|
|
@@ -301,6 +301,10 @@ export declare function captionEditNotices(result: {
|
|
|
301
301
|
removedEmphasis: readonly CaptionEmphasis[];
|
|
302
302
|
}, oldDisplayText: string): string[];
|
|
303
303
|
export declare function applyWordBookToCaptionsInSource(source: string, changes: WordBookCaptionChange[]): string;
|
|
304
|
+
export declare function applyWordBookToCaptionsInSourceWithNotices(source: string, changes: WordBookCaptionChange[]): {
|
|
305
|
+
source: string;
|
|
306
|
+
notices: string[];
|
|
307
|
+
};
|
|
304
308
|
export declare function updateCaptionTextStyleInSource(source: string, captionId: string, updates: CaptionTextStylePatch): string;
|
|
305
309
|
export declare function updateCaptionStylePresetInSource(source: string, captionIds: readonly string[], presetId: string | null, options?: {
|
|
306
310
|
catalog?: TextstyleCatalog;
|