akari-video 0.1.16 → 0.1.17
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/docs/contract-2026-08-02-preview-parity.md +2 -2
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/edit-lint/src/edit-lint.mjs +15 -0
- package/vendor/packages/edit-store/lib/edit-v2-item-write.d.ts +1 -0
- package/vendor/packages/edit-store/lib/edit-v2-item-write.js +12 -0
- package/vendor/packages/edit-store/lib/edit-v2.d.ts +1 -0
- package/vendor/packages/edit-store/lib/edit-v2.js +8 -1
- package/vendor/packages/edit-store/lib/internal-model.d.ts +1 -0
- package/vendor/packages/edit-store/lib/internal-model.js +6 -2
- package/vendor/packages/overlay-runtime/package.json +1 -1
- package/vendor/packages/schemas/edit.schema.json +7 -2
- package/vendor/packages/schemas/examples/edit-v2-html-params-invalid/edit.json +23 -0
- package/vendor/packages/schemas/examples/edit-v2-html-params-valid/edit.json +23 -0
- package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +11 -0
- package/vendor/skills/overlay-authoring/telop.md +25 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
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": {
|
|
@@ -560,8 +560,8 @@ ffmpeg の `perspective` フィルタの制約(式に時刻変数を持たな
|
|
|
560
560
|
| `cuts[].fx`(2026-08-07 実装・近似あり) | 🟡(§2.4.5。5 種対応、3 種は近似バッジ付き) | ❌(未実装) |
|
|
561
561
|
| `layers[].keyframes`(2026-08-09 実装) | ✅(§2.4.7。transform/crop は連続補間。perspective は blend:"normal" のみ・書き出しの段階保持とサンプル点で一致) | ✅(§2.4.7。同左) |
|
|
562
562
|
| `cuts[].static-image-source`(2026-08-12 実装。正本: `contract-2026-08-12-still-image-cut-source-v0.md`) | 🟡(`<img>`/`<video>` 出し分け + preview-engine ClipSession/Timeline の image 対応を実装。framing/freeze/transform は流用。実ブラウザでの対話的スクラブ・複数区間切替の実機検証は未実施。2026-08-17: 静止画が stylesheet の display:none に隠れたまま永久に出ない実機バグ(`img.style.display=''`)を是正) | 🟡(2026-08-17 実装 — task/2026-08-17-shell-still-image-cut-preview。#preview-still + gap と同じ壁時計クロックで表示。cut transform/framing/freeze/選択ドラッグは video のスタイル鏡写しで流用。タイムラインの静止画フィルムストリップ/サムネも同時是正(probeForFilmstrip の duration 必須ガードが静止画分岐を dead code 化していた)。Electron 実機での対話検証は未実施) |
|
|
563
|
-
| `output.look` |
|
|
564
|
-
| `chroma_key`(source / layer) |
|
|
563
|
+
| `output.look` | ✅(WebGL rail。chroma → LUT、intensity 対応) | ✅(WebGL rail。chroma → LUT、intensity 対応) |
|
|
564
|
+
| `chroma_key`(source / layer) | ✅(WebGL rail。source は背景合成、layer はアルファ抜き) | ✅(WebGL rail。source は背景合成、layer はアルファ抜き) |
|
|
565
565
|
| `audio.master` | 🟡(未裁定・badge のみ) | 🟡(未裁定・badge のみ) |
|
|
566
566
|
|
|
567
567
|
- `cuts[].framing`(静的クロップ / ズームキーフレーム)・`cuts[].freeze`(フリーズ)は
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
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. Run them from a full AKARI Video app installation (normally ~/.akari/app) or a monorepo checkout.]",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -683,6 +683,21 @@ function validateEditV2(edit, findings) {
|
|
|
683
683
|
});
|
|
684
684
|
}
|
|
685
685
|
|
|
686
|
+
if (kind === "html" && Object.hasOwn(item.source, "params")) {
|
|
687
|
+
const params = item.source.params;
|
|
688
|
+
const invalidEntry = isRecord(params)
|
|
689
|
+
? Object.entries(params).find(([, value]) => typeof value !== "string")
|
|
690
|
+
: ["params", params];
|
|
691
|
+
if (invalidEntry) {
|
|
692
|
+
addFinding(findings, {
|
|
693
|
+
severity: "error",
|
|
694
|
+
check: "v2.html-params",
|
|
695
|
+
message: "HTML source params must be an object whose values are strings",
|
|
696
|
+
path: `${itemPath}.source.params${invalidEntry[0] === "params" ? "" : `.${invalidEntry[0]}`}`,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
686
701
|
if (track.lane === "audio") {
|
|
687
702
|
const role = item.role ?? "sfx";
|
|
688
703
|
if (Object.hasOwn(item, "gain_db")
|
|
@@ -49,6 +49,15 @@ function resolveV2Write(parsed, command) {
|
|
|
49
49
|
if (typeof command.patch.html === 'string') {
|
|
50
50
|
htmlPath = source.path;
|
|
51
51
|
}
|
|
52
|
+
if (command.patch.params) {
|
|
53
|
+
for (const [name, value] of Object.entries(command.patch.params)) {
|
|
54
|
+
if (!name || typeof value !== 'string') {
|
|
55
|
+
throw new Error('HTML params は空でないキーと文字列値である必要があります');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
source.params = { ...source.params, ...command.patch.params };
|
|
59
|
+
editChanged = true;
|
|
60
|
+
}
|
|
52
61
|
if (command.patch.vars) {
|
|
53
62
|
source.vars = { ...recordOf(source.vars), ...command.patch.vars };
|
|
54
63
|
editChanged = true;
|
|
@@ -109,6 +118,9 @@ function resolveLegacyWrite(edit, command) {
|
|
|
109
118
|
throw new Error(`overlays[].html がファイル参照ではありません: ${command.itemId}`);
|
|
110
119
|
}
|
|
111
120
|
let editChanged = false;
|
|
121
|
+
if (command.patch.params) {
|
|
122
|
+
throw new Error('HTML params の書き戻しには edit.json version 2 が必要です');
|
|
123
|
+
}
|
|
112
124
|
if (command.patch.vars) {
|
|
113
125
|
overlay.vars = { ...recordOf(overlay.vars), ...command.patch.vars };
|
|
114
126
|
editChanged = true;
|
|
@@ -239,10 +239,17 @@ function validateItemSource(value, path, sourceIds) {
|
|
|
239
239
|
requirePositiveNumber(value.speed, `${path}.speed`);
|
|
240
240
|
return;
|
|
241
241
|
case 'html':
|
|
242
|
-
requireExactKeys(value, new Set(['kind', 'path', 'vars']), path);
|
|
242
|
+
requireExactKeys(value, new Set(['kind', 'path', 'vars', 'params']), path);
|
|
243
243
|
requireText(value.path, `${path}.path`);
|
|
244
244
|
if (hasOwn(value, 'vars'))
|
|
245
245
|
requireRecord(value.vars, `${path}.vars`);
|
|
246
|
+
if (hasOwn(value, 'params')) {
|
|
247
|
+
requireRecord(value.params, `${path}.params`);
|
|
248
|
+
for (const [name, text] of Object.entries(value.params)) {
|
|
249
|
+
if (typeof text !== 'string')
|
|
250
|
+
throw invalid(`${path}.params.${name}`, '文字列である必要があります');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
246
253
|
return;
|
|
247
254
|
case 'telop':
|
|
248
255
|
requireExactKeys(value, new Set(['kind', 'preset', 'params', 'baked']), path);
|
|
@@ -466,7 +466,8 @@ function buildV2VisualItem(item, fps, ref, pathOf, chromaKeyOf, legacyIndexCount
|
|
|
466
466
|
case 'html': {
|
|
467
467
|
const declaration = {
|
|
468
468
|
id: item.id, html: item.source.path, start: at, duration, track: ref,
|
|
469
|
-
...(item.source.vars !== undefined ? { vars: item.source.vars } : {}),
|
|
469
|
+
...(item.source.vars !== undefined ? { vars: item.source.vars } : {}),
|
|
470
|
+
...(item.source.params !== undefined ? { params: item.source.params } : {}), ...common
|
|
470
471
|
};
|
|
471
472
|
const value = {
|
|
472
473
|
id: item.id,
|
|
@@ -478,7 +479,10 @@ function buildV2VisualItem(item, fps, ref, pathOf, chromaKeyOf, legacyIndexCount
|
|
|
478
479
|
return {
|
|
479
480
|
item: {
|
|
480
481
|
id: item.id, atFrames, durationFrames, at, duration,
|
|
481
|
-
source: {
|
|
482
|
+
source: {
|
|
483
|
+
kind: 'html', html: item.source.path,
|
|
484
|
+
...(item.source.params !== undefined ? { params: item.source.params } : {})
|
|
485
|
+
},
|
|
482
486
|
declaration,
|
|
483
487
|
legacy: { collection: 'overlays', index: nextLegacyIndex(legacyIndexCounters, 'overlays'), value }
|
|
484
488
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"README.md"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"check": "node --check src/vendor/three-bundle.js && node --check src/vendor/vendor-3d-text-bundle.js && node --check src/three-runtime.js && node --check src/video-fx.js && node --check src/overlay-runtime.js && node --check src/interaction.js && node --check src/minimap.js",
|
|
13
|
+
"check": "node --check src/vendor/three-bundle.js && node --check src/vendor/vendor-3d-text-bundle.js && node --check src/three-runtime.js && node --check src/slot-params.js && node --check src/video-fx.js && node --check src/overlay-runtime.js && node --check src/interaction.js && node --check src/minimap.js",
|
|
14
14
|
"test": "node --test test-harness/*.test.mjs"
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -666,9 +666,14 @@
|
|
|
666
666
|
"properties": {
|
|
667
667
|
"kind": { "const": "html" },
|
|
668
668
|
"path": { "type": "string", "minLength": 1, "pattern": "\\S" },
|
|
669
|
-
"vars": { "type": "object" }
|
|
669
|
+
"vars": { "type": "object" },
|
|
670
|
+
"params": {
|
|
671
|
+
"type": "object",
|
|
672
|
+
"properties": {},
|
|
673
|
+
"additionalProperties": { "type": "string" }
|
|
674
|
+
}
|
|
670
675
|
},
|
|
671
|
-
"$comment": "notes-2026-08-18-timeline-latency-and-track-model.md §9。HTML はその場で描画する絵の出どころであり、素材トリムの in/out を持たない。"
|
|
676
|
+
"$comment": "notes-2026-08-18-timeline-latency-and-track-model.md §9 / contract-2026-08-22-overlay-html-slots.md §1。HTML はその場で描画する絵の出どころであり、素材トリムの in/out を持たない。params は任意のスロット名をキーにできる一方、値を string のみに閉じる。"
|
|
672
677
|
},
|
|
673
678
|
"itemSourceTelopV2": {
|
|
674
679
|
"type": "object",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 2,
|
|
3
|
+
"output": { "width": 1920, "height": 1080, "fps": 30 },
|
|
4
|
+
"sources": [],
|
|
5
|
+
"tracks": [
|
|
6
|
+
{
|
|
7
|
+
"id": "v-html",
|
|
8
|
+
"lane": "visual",
|
|
9
|
+
"items": [
|
|
10
|
+
{
|
|
11
|
+
"id": "chapter-1",
|
|
12
|
+
"at": 0,
|
|
13
|
+
"duration": 90,
|
|
14
|
+
"source": {
|
|
15
|
+
"kind": "html",
|
|
16
|
+
"path": "overlays/chapter.html",
|
|
17
|
+
"params": { "title": 1 }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 2,
|
|
3
|
+
"output": { "width": 1920, "height": 1080, "fps": 30 },
|
|
4
|
+
"sources": [],
|
|
5
|
+
"tracks": [
|
|
6
|
+
{
|
|
7
|
+
"id": "v-html",
|
|
8
|
+
"lane": "visual",
|
|
9
|
+
"items": [
|
|
10
|
+
{
|
|
11
|
+
"id": "chapter-1",
|
|
12
|
+
"at": 0,
|
|
13
|
+
"duration": 90,
|
|
14
|
+
"source": {
|
|
15
|
+
"kind": "html",
|
|
16
|
+
"path": "overlays/chapter.html",
|
|
17
|
+
"params": { "title": "第1章", "subtitle": "問題の本質" }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -134,6 +134,17 @@ test("minimum v2 fixture is valid", () => {
|
|
|
134
134
|
assert.equal(validate(fixture("edit-v2-minimal-valid")), true, JSON.stringify(validate.errors, null, 2));
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
+
test("HTML source params accepts arbitrary slot names but only string values", () => {
|
|
138
|
+
const valid = fixture("edit-v2-html-params-valid");
|
|
139
|
+
assert.equal(validate(valid), true, JSON.stringify(validate.errors, null, 2));
|
|
140
|
+
|
|
141
|
+
const invalid = fixture("edit-v2-html-params-invalid");
|
|
142
|
+
assert.equal(validate(invalid), false);
|
|
143
|
+
assert.ok(validate.errors?.some(error =>
|
|
144
|
+
error.instancePath.endsWith("/source/params/title") && error.keyword === "type"
|
|
145
|
+
), JSON.stringify(validate.errors, null, 2));
|
|
146
|
+
});
|
|
147
|
+
|
|
137
148
|
test("editV2 rejects removed top-level vocabulary as additional properties", () => {
|
|
138
149
|
for (const [key, extension] of [
|
|
139
150
|
["beats", []],
|
|
@@ -46,12 +46,36 @@ YouTube の safe zone も全端末保証ではない。オーガニック投稿
|
|
|
46
46
|
放送の左上・右上に常駐する番組名/コーナー名ラベル。番組タイトル + 現在の章タイトルを出し、章立てに追従させる定型。**素材ライブラリに入れず、この定型から都度生成してよい**(プロジェクト固有のテキスト差し替えが本体で、再利用価値は構造にしかないため)。
|
|
47
47
|
|
|
48
48
|
- **HTML/CSS で作る。画像生成にしない**: 章ごとのテキスト差し替えが必要で、画像だと章数分の生成と文字品質リスク(画像生成の日本語誤字)を抱える。HTML なら 1 断片 + 章ごとのオーバーレイエントリで済み、プレビューと書き出しが同一ソースを通る
|
|
49
|
-
- **章追従の実装**:
|
|
49
|
+
- **章追従の実装**: スロット付きの同じ断片を章数分参照し、章タイトルを各 HTML アイテムの `source.params`、区間を `at` / `duration` に持たせる(1 章 = 1 オーバーレイアイテム)。固定文言だけの既存断片は従来どおり DOM 直書きでもよい。CSS 変数へテキストを持たせない
|
|
50
50
|
- **構造の目安**: ルート 1 つ + プレート(番組名の行 + 章名の行、または 1 行連結)。位置は外側コンテナの transform(ランタイム所有)で決め、断片は見た目だけを持つ
|
|
51
51
|
- **常駐物の抑制**: 常時表示なので主張を抑える。小さめの `--font-size`、半透明の地、画面の隅で被写体・字幕・映像内 UI と重ねない。字幕(下側)と同時表示が前提なので、下側には置かない
|
|
52
52
|
- knobs の例: `--plate-bg`、`--accent`、`--font-size`、`--radius`。既定値は `var(--name, fallback)` で公開する
|
|
53
53
|
- 章替わりの出入りは短い opacity / translate のみ。保持中は動かさない(アニメーション節の原則どおり)
|
|
54
54
|
|
|
55
|
+
## テキストスロット(data-akari-slot)
|
|
56
|
+
|
|
57
|
+
1 本の HTML を複数アイテムで共有し、文字だけを `edit.json` に分離するときに使う。
|
|
58
|
+
|
|
59
|
+
```html
|
|
60
|
+
<span data-akari-slot="title">既定タイトル</span>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"source": {
|
|
66
|
+
"kind": "html",
|
|
67
|
+
"path": "overlays/chapter-tag.html",
|
|
68
|
+
"params": { "title": "第1章 問題の本質" }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- **宣言**: 差し替えるテキスト要素そのものへ `data-akari-slot="<name>"` を付ける。スロット内はプレーンテキストだけにし、子要素や別スロットを入れ子にしない。ランタイムは値を `textContent` へ入れるため、`<b>` 等を params に書いてもタグにはならない
|
|
74
|
+
- **既定テキスト**: 要素内へ必ず人が読める既定値を書く。対応する params キーが無いアイテムでは、その既定値がそのまま表示される
|
|
75
|
+
- **命名**: `^[a-z][a-z0-9_-]*$` の意味名を使う(例: `title`、`chapter-title`、`speaker_name`)。連番や表示文言そのものを名前にせず、同じ名前を複数要素へ使う場合は同じ文字を表示する意図に限る
|
|
76
|
+
- **共有境界**: 色・余白・書体・アニメーションはテンプレ HTML/CSS、インスタンスごとの文字は `source.params` に置く。スロットをダブルクリック編集した場合も、テンプレファイルではなく params だけが更新される
|
|
77
|
+
- **非スロットとの互換**: `data-akari-slot` の無い断片は従来どおり DOM の文字を編集し、参照先 HTML ファイルへ書き戻す
|
|
78
|
+
|
|
55
79
|
## アニメーション
|
|
56
80
|
|
|
57
81
|
- 字幕の出入りは短い opacity / translate を使い、保持中は動かさない。
|