akari-video 1.0.3 → 1.1.0-beta.2
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/self-update.mjs +11 -1
- package/src/update-check.mjs +34 -29
- package/src/version.mjs +23 -0
- package/vendor/.akari-capability-sources.json +1 -0
- package/vendor/docs/contract-2026-08-28-gpu-export-v0.md +4 -0
- package/vendor/docs/contract-2026-09-02-audio-envelope-v1.md +9 -0
- package/vendor/docs/contract-2026-10-06-timeline-ripple-v0.md +33 -0
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/akari-tools/README.md +1 -1
- package/vendor/packages/edit-lint/src/engine-capabilities.json +2 -0
- package/vendor/packages/edit-lint/src/lint/edit-v2.mjs +10 -0
- package/vendor/packages/edit-store/lib/caption-store.d.ts +6 -0
- package/vendor/packages/edit-store/lib/caption-store.js +3 -2
- package/vendor/packages/edit-store/lib/cut-ranges.js +12 -50
- package/vendor/packages/edit-store/lib/edit-v2.d.ts +9 -0
- package/vendor/packages/edit-store/lib/edit-v2.js +12 -2
- package/vendor/packages/edit-store/lib/envelope.d.ts +10 -0
- package/vendor/packages/edit-store/lib/envelope.js +95 -0
- package/vendor/packages/edit-store/lib/generated/edit-v2-keys.d.ts +2 -2
- package/vendor/packages/edit-store/lib/generated/edit-v2-keys.js +3 -1
- package/vendor/packages/edit-store/lib/index.d.ts +1 -0
- package/vendor/packages/edit-store/lib/index.js +1 -0
- package/vendor/packages/edit-store/lib/migrate/index.js +15 -4
- package/vendor/packages/edit-store/lib/ripple.d.ts +42 -0
- package/vendor/packages/edit-store/lib/ripple.js +438 -0
- package/vendor/packages/edit-store/lib/shape-markup.js +5 -2
- package/vendor/packages/edit-store/lib/webview-kernel.js +104 -3
- package/vendor/packages/overlay-runtime/src/interaction.css +7 -0
- package/vendor/packages/overlay-runtime/src/interaction.js +68 -5
- package/vendor/packages/schemas/edit.schema.json +11 -1
- package/vendor/packages/schemas/engine-capabilities.json +2 -0
- package/vendor/packages/schemas/test/audio-fade-shape.test.mjs +31 -0
- package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +12 -0
- package/vendor/packages/schemas/test/engine-capabilities.test.mjs +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.1.0-beta.2",
|
|
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/self-update.mjs
CHANGED
|
@@ -4,7 +4,8 @@ import { spawnSync } from 'node:child_process';
|
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
7
|
-
import { resolveAkariHome } from './update-check.mjs';
|
|
7
|
+
import { readUpdateChannel, resolveAkariHome } from './update-check.mjs';
|
|
8
|
+
import { compareVersions } from './version.mjs';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* `akari update` の実適用(update-and-versioning 契約(内部リポ)§11):
|
|
@@ -202,6 +203,15 @@ export async function stageSelfUpdate({
|
|
|
202
203
|
}
|
|
203
204
|
|
|
204
205
|
const version = feed.product;
|
|
206
|
+
if (readUpdateChannel(env) === 'stable' && version?.includes('-')) {
|
|
207
|
+
log('安定版だけを受け取る設定です。');
|
|
208
|
+
return { ok: false, reason: 'channel-mismatch' };
|
|
209
|
+
}
|
|
210
|
+
const installedVersion = readVersionAt(resolveAppDir(env));
|
|
211
|
+
if (installedVersion && compareVersions(version, installedVersion) < 0) {
|
|
212
|
+
log('現在より古い版は適用できません。');
|
|
213
|
+
return { ok: false, reason: 'older-version' };
|
|
214
|
+
}
|
|
205
215
|
const stagingRoot = resolveStagingRoot(env);
|
|
206
216
|
const stagingDir = resolveStagingDir(env, version);
|
|
207
217
|
const downloadPath = join(stagingRoot, `.download-${version}.tar.gz`);
|
package/src/update-check.mjs
CHANGED
|
@@ -5,6 +5,8 @@ import { dirname, join } from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
7
7
|
import { isRunningFromAppDir, resolveStagingDir, stageSelfUpdate, swapStagedApp } from './self-update.mjs';
|
|
8
|
+
import { compareVersions } from './version.mjs';
|
|
9
|
+
export { compareVersions } from './version.mjs';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* CLI(と、同じ契約に従うシェル)の更新検知・通知の共通ロジック
|
|
@@ -35,6 +37,7 @@ import { isRunningFromAppDir, resolveStagingDir, stageSelfUpdate, swapStagedApp
|
|
|
35
37
|
*/
|
|
36
38
|
|
|
37
39
|
export const DEFAULT_UPDATE_FEED_URL = 'https://github.com/AkariLabs/akari-video/releases/download/updates/latest.json';
|
|
40
|
+
export const PRERELEASE_UPDATE_FEED_URL = 'https://github.com/AkariLabs/akari-video/releases/download/updates/prerelease.json';
|
|
38
41
|
|
|
39
42
|
const CACHE_SCHEMA = 1;
|
|
40
43
|
const FETCH_TIMEOUT_MS = 5000;
|
|
@@ -42,8 +45,18 @@ const FETCH_TIMEOUT_MS = 5000;
|
|
|
42
45
|
const THIS_FILE = fileURLToPath(import.meta.url);
|
|
43
46
|
const PACKAGE_ROOT = dirname(dirname(THIS_FILE));
|
|
44
47
|
|
|
45
|
-
export function
|
|
46
|
-
return
|
|
48
|
+
export function resolveUpdateChannel(value) {
|
|
49
|
+
return value === 'prerelease' ? 'prerelease' : 'stable';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function readUpdateChannel(env = process.env) {
|
|
53
|
+
try { return resolveUpdateChannel(JSON.parse(readFileSync(join(resolveAkariHome(env), 'update-preferences.json'), 'utf8')).channel); }
|
|
54
|
+
catch { return 'stable'; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 明示 URL はテスト・運用の完全上書き。チャンネルは評価側でも独立に検査する。
|
|
58
|
+
export function resolveFeedUrl(env = process.env, channel = readUpdateChannel(env)) {
|
|
59
|
+
return env.AKARI_UPDATE_FEED_URL || (channel === 'prerelease' ? PRERELEASE_UPDATE_FEED_URL : DEFAULT_UPDATE_FEED_URL);
|
|
47
60
|
}
|
|
48
61
|
|
|
49
62
|
/** `~/.akari`(既定)または `AKARI_HOME`(テスト用にルートを差し替え可能)。 */
|
|
@@ -112,26 +125,6 @@ export function resolveInstalledVersionInfo({ env = process.env, cliVersion = re
|
|
|
112
125
|
};
|
|
113
126
|
}
|
|
114
127
|
|
|
115
|
-
/** "major.minor.patch" の先頭 3 要素だけを数値比較する(prerelease 考慮不要 — 契約 D4: stable のみ)。 */
|
|
116
|
-
export function compareVersions(a, b) {
|
|
117
|
-
const pa = parseVersionTriplet(a);
|
|
118
|
-
const pb = parseVersionTriplet(b);
|
|
119
|
-
if (!pa || !pb) {
|
|
120
|
-
return 0;
|
|
121
|
-
}
|
|
122
|
-
for (let i = 0; i < 3; i++) {
|
|
123
|
-
if (pa[i] !== pb[i]) {
|
|
124
|
-
return pa[i] < pb[i] ? -1 : 1;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return 0;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function parseVersionTriplet(value) {
|
|
131
|
-
const match = typeof value === 'string' ? value.trim().match(/^(\d+)\.(\d+)\.(\d+)/) : null;
|
|
132
|
-
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
128
|
/** `cache.feed` が最低限の形をしているか(壊れたフィードを弾く)。 */
|
|
136
129
|
export function isValidFeedShape(feed) {
|
|
137
130
|
return !!feed && typeof feed === 'object' && typeof feed.schema === 'number' && typeof feed.product === 'string';
|
|
@@ -141,7 +134,13 @@ export function isValidFeedShape(feed) {
|
|
|
141
134
|
export function readCacheSync(cachePath) {
|
|
142
135
|
try {
|
|
143
136
|
const parsed = JSON.parse(readFileSync(cachePath, 'utf8'));
|
|
144
|
-
|
|
137
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
138
|
+
// cli.mjs もこの関数でキャッシュを読む。設定切替直後の手動 update を含めて遮断。
|
|
139
|
+
let channel = 'stable';
|
|
140
|
+
try { channel = resolveUpdateChannel(JSON.parse(readFileSync(join(dirname(cachePath), 'update-preferences.json'), 'utf8')).channel); }
|
|
141
|
+
catch { /* 未設定は stable */ }
|
|
142
|
+
return channel === 'stable' && parsed.feed?.product?.includes('-')
|
|
143
|
+
? { ...parsed, feed: null, staged: undefined } : parsed;
|
|
145
144
|
} catch {
|
|
146
145
|
return null;
|
|
147
146
|
}
|
|
@@ -155,7 +154,7 @@ function writeCacheSync(cachePath, cache) {
|
|
|
155
154
|
/**
|
|
156
155
|
* キャッシュと現在版から、新版通知を出すべきかを判定する(同期・純粋関数・I/O なし)。
|
|
157
156
|
*/
|
|
158
|
-
export function evaluateUpdateStatus({ currentVersion, cache, cliVersion = currentVersion, appVersion = null, source = 'cli-fallback', installRefStatus, installRefPath, installRefNeedsRepair, managedApp, mismatch = false }) {
|
|
157
|
+
export function evaluateUpdateStatus({ currentVersion, cache, channel = 'stable', feedUrl, cliVersion = currentVersion, appVersion = null, source = 'cli-fallback', installRefStatus, installRefPath, installRefNeedsRepair, managedApp, mismatch = false }) {
|
|
159
158
|
const versionDetails = {
|
|
160
159
|
currentVersion,
|
|
161
160
|
cliVersion,
|
|
@@ -168,7 +167,8 @@ export function evaluateUpdateStatus({ currentVersion, cache, cliVersion = curre
|
|
|
168
167
|
mismatch
|
|
169
168
|
};
|
|
170
169
|
const feed = cache?.feed;
|
|
171
|
-
if (!isValidFeedShape(feed))
|
|
170
|
+
if (!isValidFeedShape(feed) || (channel === 'stable' && /-/.test(feed.product))
|
|
171
|
+
|| (cache?.feed_url && feedUrl && cache.feed_url !== feedUrl)) {
|
|
172
172
|
return { available: false, ...versionDetails };
|
|
173
173
|
}
|
|
174
174
|
const latest = feed.product;
|
|
@@ -200,7 +200,8 @@ export function checkForUpdateSync({ currentVersion, versionInfo, env = process.
|
|
|
200
200
|
?? (currentVersion === undefined
|
|
201
201
|
? resolveInstalledVersionInfo({ env })
|
|
202
202
|
: { cliVersion: currentVersion, appVersion: null, currentVersion, source: 'cli-fallback', mismatch: false });
|
|
203
|
-
return evaluateUpdateStatus({ ...versions, currentVersion: currentVersion ?? versions.currentVersion, cache
|
|
203
|
+
return evaluateUpdateStatus({ ...versions, currentVersion: currentVersion ?? versions.currentVersion, cache,
|
|
204
|
+
channel: readUpdateChannel(env), feedUrl: resolveFeedUrl(env) });
|
|
204
205
|
}
|
|
205
206
|
|
|
206
207
|
/** 「この版の通知を今後出さない」を記録する(同期・ローカル I/O のみ)。 */
|
|
@@ -234,7 +235,8 @@ export async function maybeStageInBackground({ env = process.env, feed, fetchImp
|
|
|
234
235
|
if (!appComponent?.url || !appComponent?.sha256) {
|
|
235
236
|
return null;
|
|
236
237
|
}
|
|
237
|
-
if (
|
|
238
|
+
if ((readUpdateChannel(env) === 'stable' && feed.product.includes('-'))
|
|
239
|
+
|| compareVersions(feed.product, resolveInstalledVersionInfo({ env }).currentVersion) <= 0) {
|
|
238
240
|
return null;
|
|
239
241
|
}
|
|
240
242
|
// npm 側 CLI と本体が分離していても、install-ref があれば管理対象の本体を更新できる。
|
|
@@ -262,7 +264,7 @@ export async function refreshUpdateFeed({ env = process.env, fetchImpl = globalT
|
|
|
262
264
|
return;
|
|
263
265
|
}
|
|
264
266
|
const feed = await response.json();
|
|
265
|
-
if (!isValidFeedShape(feed)) {
|
|
267
|
+
if (!isValidFeedShape(feed) || (readUpdateChannel(env) === 'stable' && feed.product.includes('-'))) {
|
|
266
268
|
return null;
|
|
267
269
|
}
|
|
268
270
|
const existing = readCacheSync(cachePath);
|
|
@@ -270,6 +272,7 @@ export async function refreshUpdateFeed({ env = process.env, fetchImpl = globalT
|
|
|
270
272
|
schema: CACHE_SCHEMA,
|
|
271
273
|
fetched_at: new Date().toISOString(),
|
|
272
274
|
feed,
|
|
275
|
+
feed_url: feedUrl,
|
|
273
276
|
dismissed: existing?.dismissed ?? {}
|
|
274
277
|
};
|
|
275
278
|
writeCacheSync(cachePath, next);
|
|
@@ -347,7 +350,9 @@ export function maybeApplyPendingUpdateOnLaunch({ env = process.env, log = () =>
|
|
|
347
350
|
const cache = readCacheSync(cachePath);
|
|
348
351
|
const staged = cache?.staged;
|
|
349
352
|
const feed = cache?.feed;
|
|
350
|
-
if (!staged?.version || !isValidFeedShape(feed) || staged.version !== feed.product
|
|
353
|
+
if (!staged?.version || !isValidFeedShape(feed) || staged.version !== feed.product
|
|
354
|
+
|| (readUpdateChannel(env) === 'stable' && feed.product.includes('-'))
|
|
355
|
+
|| (cache.feed_url && cache.feed_url !== resolveFeedUrl(env))) {
|
|
351
356
|
return { applied: false, reason: 'no-matching-staged-update' };
|
|
352
357
|
}
|
|
353
358
|
|
package/src/version.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** npm 依存ゼロの launcher 用 semver 比較。build metadata は順序に影響しない。 */
|
|
2
|
+
export function compareVersions(a, b) {
|
|
3
|
+
const parse = value => {
|
|
4
|
+
const match = typeof value === 'string' ? value.trim().match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.-]+)?$/) : null;
|
|
5
|
+
if (!match) return null;
|
|
6
|
+
const pre = match[4]?.split('.') ?? null;
|
|
7
|
+
if (pre?.some(id => /^0\d+$/.test(id))) return null;
|
|
8
|
+
return { core: match.slice(1, 4).map(Number), pre };
|
|
9
|
+
};
|
|
10
|
+
const left = parse(a), right = parse(b);
|
|
11
|
+
if (!left || !right) return 0;
|
|
12
|
+
for (let i = 0; i < 3; i++) if (left.core[i] !== right.core[i]) return Math.sign(left.core[i] - right.core[i]);
|
|
13
|
+
if (!left.pre || !right.pre) return left.pre === right.pre ? 0 : left.pre ? -1 : 1;
|
|
14
|
+
for (let i = 0; i < Math.min(left.pre.length, right.pre.length); i++) {
|
|
15
|
+
const x = left.pre[i], y = right.pre[i];
|
|
16
|
+
if (x === y) continue;
|
|
17
|
+
const xn = /^\d+$/.test(x), yn = /^\d+$/.test(y);
|
|
18
|
+
if (xn && yn) return Math.sign(BigInt(x) > BigInt(y) ? 1 : -1);
|
|
19
|
+
if (xn !== yn) return xn ? -1 : 1;
|
|
20
|
+
return x < y ? -1 : 1;
|
|
21
|
+
}
|
|
22
|
+
return Math.sign(left.pre.length - right.pre.length);
|
|
23
|
+
}
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"docs/contract-2026-10-02-fragment-font-subset-v0.md",
|
|
83
83
|
"docs/contract-2026-10-02-textstyle-v1-rich.md",
|
|
84
84
|
"docs/contract-2026-10-04-asset-tier-v1.md",
|
|
85
|
+
"docs/contract-2026-10-06-timeline-ripple-v0.md",
|
|
85
86
|
"packages/akari-launcher/package.json",
|
|
86
87
|
"packages/akari-launcher/README.md",
|
|
87
88
|
"packages/akari-tools/package.json",
|
|
@@ -517,6 +517,10 @@ cut 段は `cut-audio.mp4`、尺延長が必要な場合は続けて `cut-audio-
|
|
|
517
517
|
|
|
518
518
|
## 12. 採寸不安定の根治・実行時フォールバック・生フレーム dump(2026-08-30 追記)
|
|
519
519
|
|
|
520
|
+
### 字幕の文字送り(2026-10-06 追記)
|
|
521
|
+
|
|
522
|
+
`text_style.animation` の `typewriter` は書記素ごとの span を時刻に合わせて表示する。GPU 字幕スプライトはこの文字単位の表示に対応しないため、適格判定で除外する。`--engine auto` は OSR を選び、理由には「文字送りは OSR で書き出します」と表示する。明示的な GPU 指定は従来どおり適格判定で拒否する。
|
|
523
|
+
|
|
520
524
|
### 12.1 事実(2026-08-30・capture-v2-engine レーンの実測と司令塔のコード確認)
|
|
521
525
|
|
|
522
526
|
- 内部 fieldtest 案件(11 秒・1080p・字幕 **3 cue・`words[]` 無し・style 無指定**・HTML オーバーレイ 2・LUT)で、
|
|
@@ -89,3 +89,12 @@ schema と reader は型・範囲・時刻順を error にする。lint は実
|
|
|
89
89
|
| 許容する差 | デコーダとサンプル境界 | デコーダとサンプル境界 |
|
|
90
90
|
|
|
91
91
|
afftdn、loudnorm、true peak guard は最終マスターだけの責務であり、プレビューには実装しない。
|
|
92
|
+
|
|
93
|
+
## 7. フェードの形
|
|
94
|
+
|
|
95
|
+
音声アイテムは `fade_in_shape` / `fade_out_shape` を任意に持つ。省略時は `linear` とし、
|
|
96
|
+
既存のフェード秒数、クランプ規則、フィルタ文字列を維持する。進行率 `p`(0〜1)に対する
|
|
97
|
+
ゲイン係数は `linear` = `p`、`equal_power` = `sin(pπ/2)`、
|
|
98
|
+
`s_curve` = `(1−cos(pπ))/2`、`slow` = `p²` とする。フェードアウトは時間方向を反転する。
|
|
99
|
+
書き出しの `afade` 曲線は順に `tri`、`qsin`、`hsin`、`qua` を使う。
|
|
100
|
+
プレビューではフェード区間を少なくとも 16 区間の折れ線で近似する。
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
lifecycle: implemented
|
|
3
|
+
created: 2026-10-06
|
|
4
|
+
updated: 2026-10-06
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# タイムラインの範囲操作と詰めカーネル v0
|
|
8
|
+
|
|
9
|
+
実装モジュールは `packages/edit-store/src/ripple.ts`。`cut-ranges.ts` はこのカーネルを呼び出す。edit.json v2 の時刻は整数フレーム。`tracks[]` の配列順は合成順として維持する。範囲編集関数は入力を変更せず `{ edit, changed, removedFrames?, blocked?, reason? }` を返す。`changed: false` では `reason` に操作不能の理由を入れる。`setTrackRippleMode` は新しい edit を直接返し、`findGapAt` と `editPoints` は読み取り値を返す。
|
|
10
|
+
|
|
11
|
+
## トラックの札
|
|
12
|
+
|
|
13
|
+
`tracks[]` に任意の `target` / `sync` 真偽値を保存する。`resolveTrackRippleMode(track)` は `target: true` を `cut`、`target: false, sync: true` を `shift`、`target: false` とそれ以外の組を `fixed` とする。両キー省略時は visual が `cut`、audio は 1 件以上の全アイテムが `role: "bgm"` なら `fixed`、それ以外は `cut`。`sync` だけ指定したときは true が `shift`、false が `fixed`。`setTrackRippleMode` は `cut = (true,true)`、`shift = (false,true)`、`fixed = (false,false)` を保存する。
|
|
14
|
+
|
|
15
|
+
操作ごとの `modeOverride` は一時指定で、`lockedTrackIds` は常に固定として優先する。固定トラックは範囲操作で変えない。選択アイテムを明示して消す場合はそのトラックを削除対象とするが、ロック中なら操作全体を拒否する。
|
|
16
|
+
|
|
17
|
+
| 関数 | 内容 |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `splitAtFrame` | 再生ヘッドをまたぐ cut トラックの media / html / telop / filter / 独立音声を分割。`itemIds` または `trackIds` で絞れる。リンクした映像・音声は同時に分割する。リンク相手がロック行にある場合、または左右どちらかが `ceil(fps × 0.15)` フレーム未満なら、全体を拒否して `changed: false` と `reason: "リンク相手が固定中か、片側が最小尺未満です"` を返す。 |
|
|
20
|
+
| `liftRange` | cut トラックの範囲内を取り除き、隙間を残す。 |
|
|
21
|
+
| `extractRange` | cut トラックの範囲内を取り除き、後続を範囲長だけ前へ送る。shift トラックは中身を取り除かず、範囲終端以降に始まるアイテムだけを前へ送る。 |
|
|
22
|
+
| `rippleDeleteItems` | 選択アイテムの区間を詰める。他トラックは中身を消さずに後続を送る。リンク相手も既定で対象とし、`oneSide` で片側だけにできる。離れた選択は右から処理する。 |
|
|
23
|
+
| `findGapAt` / `closeGapAt` | 指定トラックでフレームを含む先頭またはアイテム間の隙間を見つけ、その長さだけ後続を送る。末尾の空きは対象外。 |
|
|
24
|
+
| `rippleTrimToPlayhead` | 最も近い前後の編集点から再生ヘッドまでを `extractRange` する。編集点が無ければ理由を返す。 |
|
|
25
|
+
| `editPoints` | cut トラックのアイテム端を昇順・重複なしで返す。 |
|
|
26
|
+
| `compactTrackGaps` | cut の visual トラックにある media だけを配列順に詰める。`fromItemId` があればそのアイテムの後続だけを詰める。既定では anchor 付きアイテムを直接動かさない。 |
|
|
27
|
+
| `applyCutRanges` | 素材秒で重なる media を選び、共通の区間除去部品で切除する。触った visual トラックの media だけを `at = 0` から配列順に詰める旧挙動を保つ。 |
|
|
28
|
+
|
|
29
|
+
shift は前のアイテムの終端より前に入らない。必要な移動量を確保できなかったアイテムの id を `blocked` に返し、重なりを作らない。`anchor` を持つアイテムは shift で直接動かさない。cut トラックなら範囲内の削除対象にはなる。位置の再解決は `refreshItemAnchors` の責務とする。`applyCutRanges` は旧挙動を維持するため、`compactTrackGaps` に `includeAnchored: true` を渡して触ったトラックの media を詰める。
|
|
30
|
+
|
|
31
|
+
分割片の `source.in/out` は、素材秒をアイテムのフレーム比率(speed 込み)で按分する。音声の `keyframes[].gain_db` と映像の数値キーフレームは境界点を両片に入れ、ローカルフレーム時刻へ振り分ける。境界値は区間の終点側キーの `easing` を `easingProgress` で評価する。文字列指定は全プロパティ、Record 指定はプロパティ別(例: `gain_db`、`opacity`、`transform.x`)に適用し、左片の新しい境界点にも終点側の easing を引き継ぐ。線形区間の全フレーム値は分割前と一致する。非線形区間は境界値を一致させるが、分割後の区間内部の曲線は近似になる。`fade_in` は左片、`fade_out` は右片だけに残し、ダッキングなど他の属性は両片へ複製する。リンク音声の右片は映像の右片へ `link` を付け替える。
|
|
32
|
+
|
|
33
|
+
字幕本文は `captions.json` のソース秒が正本であり、ここでは変更しない。切除後の表示位置は `timeline-map` の `sourceToOutput` で追従する。通常の Delete は隙間を残す。詰める処理は明示した操作でだけ行う。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.1.0-beta.2",
|
|
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": [
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- `akari media waveform <target>` — 波形、無音区間、ラウドネスを調べる。
|
|
9
9
|
- `akari media transcribe <target>` — ローカル既定の時刻付き文字起こしを返す。
|
|
10
10
|
- `akari media transcribe-diff <target> [--engines a,b,c]` — 最新のエンジン別文字起こしを文字単位で比較し、`diff.json` と JSON 要約を返す。句読点だけの差は除外し、1 エンジンなら比較を省略する。
|
|
11
|
-
- `akari media transcribe-cuts <target> [--basis b] [--silence-min
|
|
11
|
+
- `akari media transcribe-cuts <target> [--basis b] [--silence-min 0.45] [--silence-break 3.0] [--silence-keep 0.15]` — フィラー・言い直し・無音・未認識を `cuts.json` に候補として保存し、JSON 要約を返す。既存の採否を引き継ぎ、手直しした字幕に重なる候補は既定 OFF にする。
|
|
12
12
|
- `akari media audio-level <projectDir>` — `gain_db` 未指定の音声素材を測り、役割別の提案値を表にする。`--write` で `edit.json` へ数値を保存し、`--json` で JSON 配列を返す。
|
|
13
13
|
|
|
14
14
|
## 音声素材の挿入レベル
|
|
@@ -57,6 +57,8 @@
|
|
|
57
57
|
{ "path": "tracks[].items[].gain_db", "applies_to": ["audio"], "gpu": "other-subsystem", "osr": "other-subsystem", "consumer": "render-cut audio mix", "evidence": "packages/edit-store/src/internal-model.ts buildV2AudioItem gainDb projection" },
|
|
58
58
|
{ "path": "tracks[].items[].fade_in", "applies_to": ["audio"], "gpu": "other-subsystem", "osr": "other-subsystem", "consumer": "render-cut audio mix", "evidence": "packages/edit-store/src/internal-model.ts buildV2AudioItem fadeIn/fade_in projection" },
|
|
59
59
|
{ "path": "tracks[].items[].fade_out", "applies_to": ["audio"], "gpu": "other-subsystem", "osr": "other-subsystem", "consumer": "render-cut audio mix", "evidence": "packages/edit-store/src/internal-model.ts buildV2AudioItem fadeOut/fade_out projection" },
|
|
60
|
+
{ "path": "tracks[].items[].fade_in_shape", "applies_to": ["audio"], "gpu": "other-subsystem", "osr": "other-subsystem", "consumer": "render-cut audio mix", "evidence": "packages/edit-store/src/envelope.ts projectAudioFadeShapes/audioFadeFfmpegCurve; packages/render-cut/src/plan.mjs fadeCurveOption" },
|
|
61
|
+
{ "path": "tracks[].items[].fade_out_shape", "applies_to": ["audio"], "gpu": "other-subsystem", "osr": "other-subsystem", "consumer": "render-cut audio mix", "evidence": "packages/edit-store/src/envelope.ts projectAudioFadeShapes/audioFadeFfmpegCurve; packages/render-cut/src/plan.mjs fadeCurveOption" },
|
|
60
62
|
{ "path": "tracks[].items[].ducking", "applies_to": ["audio"], "gpu": "other-subsystem", "osr": "other-subsystem", "consumer": "render-cut audio mix", "evidence": "packages/edit-store/src/internal-model.ts buildV2AudioItem ducking projection" },
|
|
61
63
|
{ "path": "tracks[].items[].duck_db", "applies_to": ["audio"], "gpu": "other-subsystem", "osr": "other-subsystem", "consumer": "render-cut audio mix", "evidence": "packages/edit-store/src/internal-model.ts buildV2AudioItem duck projection; packages/edit-store/src/envelope.ts computeDuckEnvelope" },
|
|
62
64
|
{ "path": "tracks[].items[].duck_attack", "applies_to": ["audio"], "gpu": "other-subsystem", "osr": "other-subsystem", "consumer": "render-cut audio mix", "evidence": "packages/edit-store/src/envelope.ts computeDuckEnvelope attack ramp" },
|
|
@@ -460,6 +460,16 @@ export function validateEditV2(edit, findings) {
|
|
|
460
460
|
path: `${itemPath}.${field}`,
|
|
461
461
|
});
|
|
462
462
|
}
|
|
463
|
+
for (const field of ["fade_in_shape", "fade_out_shape"]) {
|
|
464
|
+
if (!Object.hasOwn(item, field)
|
|
465
|
+
|| ["linear", "equal_power", "s_curve", "slow"].includes(item[field])) continue;
|
|
466
|
+
addFinding(findings, {
|
|
467
|
+
severity: "error",
|
|
468
|
+
check: "audio.fade-shape",
|
|
469
|
+
message: `${field} must be linear, equal_power, s_curve, or slow`,
|
|
470
|
+
path: `${itemPath}.${field}`,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
463
473
|
if (isRecord(item.source)) {
|
|
464
474
|
if (role === "narration") {
|
|
465
475
|
for (const field of ["in", "out"]) {
|
|
@@ -191,6 +191,12 @@ export interface CaptionTextStylePatch {
|
|
|
191
191
|
ease?: string | null;
|
|
192
192
|
amp?: number | null;
|
|
193
193
|
} | null;
|
|
194
|
+
loop?: {
|
|
195
|
+
id: string;
|
|
196
|
+
durationSec?: number;
|
|
197
|
+
ease?: string | null;
|
|
198
|
+
amp?: number | null;
|
|
199
|
+
} | null;
|
|
194
200
|
out?: {
|
|
195
201
|
id: string;
|
|
196
202
|
durationSec?: number;
|
|
@@ -1607,7 +1607,7 @@ function validateTextStylePatch(updates) {
|
|
|
1607
1607
|
throw new Error('字幕アニメの設定が不正です。');
|
|
1608
1608
|
}
|
|
1609
1609
|
if (updates.animation && typeof updates.animation === 'object') {
|
|
1610
|
-
for (const slot of [updates.animation.in, updates.animation.out]) {
|
|
1610
|
+
for (const slot of [updates.animation.in, updates.animation.loop, updates.animation.out]) {
|
|
1611
1611
|
if (slot === undefined || slot === null)
|
|
1612
1612
|
continue;
|
|
1613
1613
|
if (!slot || typeof slot !== 'object' || typeof slot.id !== 'string'
|
|
@@ -1786,13 +1786,14 @@ function updateAnimationStyleObject(source, updates, label) {
|
|
|
1786
1786
|
if (!located) {
|
|
1787
1787
|
const created = {
|
|
1788
1788
|
...(updates.in ? { in: animationSlotToJson(updates.in) } : {}),
|
|
1789
|
+
...(updates.loop ? { loop: animationSlotToJson(updates.loop) } : {}),
|
|
1789
1790
|
...(updates.out ? { out: animationSlotToJson(updates.out) } : {})
|
|
1790
1791
|
};
|
|
1791
1792
|
return Object.keys(created).length > 0 ? appendJsonProperty(source, 'animation', created) : source;
|
|
1792
1793
|
}
|
|
1793
1794
|
const object = locateTopLevelObjectProperty(source, 'animation', label);
|
|
1794
1795
|
let next = object.text;
|
|
1795
|
-
for (const [slotName, slot] of [['in', updates.in], ['out', updates.out]]) {
|
|
1796
|
+
for (const [slotName, slot] of [['in', updates.in], ['loop', updates.loop], ['out', updates.out]]) {
|
|
1796
1797
|
if (slot === undefined)
|
|
1797
1798
|
continue;
|
|
1798
1799
|
if (slot === null) {
|
|
@@ -2,13 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.detectEditVersion = detectEditVersion;
|
|
4
4
|
exports.applyCutRanges = applyCutRanges;
|
|
5
|
-
/**
|
|
6
|
-
* v2 の分割・削除は akari-annotations の edit-v2-mutations.ts にある
|
|
7
|
-
* splitItem :597-626 / removeItem :567-572 を出所とし、同じ按分規則を使う。
|
|
8
|
-
* edit-store から Theia 拡張へ逆依存できないため、この最小部分だけを複製している。
|
|
9
|
-
*/
|
|
5
|
+
/** Source-time selection stays local; retained pieces share the ripple kernel's slicing rule. */
|
|
10
6
|
const edit_store_1 = require("./edit-store");
|
|
11
7
|
const edit_v2_1 = require("./edit-v2");
|
|
8
|
+
const ripple_1 = require("./ripple");
|
|
12
9
|
const LEGACY_EDGE_SECONDS = 0.15;
|
|
13
10
|
function detectEditVersion(source) {
|
|
14
11
|
const version = JSON.parse(source).version;
|
|
@@ -115,21 +112,10 @@ function applyV2(source, ranges, opts) {
|
|
|
115
112
|
if (!matched)
|
|
116
113
|
warnings.push(`カット対象が見つかりません: ${range.in}–${range.out}`);
|
|
117
114
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
continue;
|
|
123
|
-
let cursor = 0;
|
|
124
|
-
for (const item of track.items) {
|
|
125
|
-
if (item.source.kind !== 'media')
|
|
126
|
-
continue;
|
|
127
|
-
item.at = cursor;
|
|
128
|
-
cursor += item.duration;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
(0, edit_v2_1.readEditV2)(edit);
|
|
132
|
-
return { source: `${JSON.stringify(edit, null, 2)}\n`, removedFrames, warnings };
|
|
115
|
+
const modeOverride = Object.fromEntries(edit.tracks.map(track => [track.id, affectedTrackIds.has(track.id) ? 'cut' : 'fixed']));
|
|
116
|
+
const compacted = (0, ripple_1.compactTrackGaps)(edit, { modeOverride, includeAnchored: true }).edit;
|
|
117
|
+
(0, edit_v2_1.readEditV2)(compacted);
|
|
118
|
+
return { source: `${JSON.stringify(compacted, null, 2)}\n`, removedFrames, warnings };
|
|
133
119
|
}
|
|
134
120
|
function copyRangeMetadata(target, range) {
|
|
135
121
|
if (range.reason !== undefined)
|
|
@@ -164,37 +150,13 @@ function splitAndRemove(item, overlapIn, overlapOut, edit) {
|
|
|
164
150
|
const endOffset = clampFrame(Math.round((overlapOut - mediaItem.source.in) / sourceDuration * mediaItem.duration), mediaItem.duration);
|
|
165
151
|
if (endOffset <= startOffset)
|
|
166
152
|
return { items: [item], removedFrames: 0 };
|
|
167
|
-
const items = [];
|
|
168
|
-
if (startOffset > 0) {
|
|
169
|
-
const first = cloneItem(mediaItem);
|
|
170
|
-
first.duration = startOffset;
|
|
171
|
-
first.source.out = mediaItem.source.in + sourceDuration * startOffset / mediaItem.duration;
|
|
172
|
-
items.push(first);
|
|
173
|
-
}
|
|
174
|
-
if (endOffset < mediaItem.duration) {
|
|
175
|
-
const second = cloneItem(mediaItem);
|
|
176
|
-
second.id = nextItemId(edit, `${mediaItem.id}-split`);
|
|
177
|
-
second.at = mediaItem.at + endOffset;
|
|
178
|
-
second.duration = mediaItem.duration - endOffset;
|
|
179
|
-
second.source.in = mediaItem.source.in + sourceDuration * endOffset / mediaItem.duration;
|
|
180
|
-
items.push(second);
|
|
181
|
-
}
|
|
182
|
-
return { items, removedFrames: endOffset - startOffset };
|
|
183
|
-
}
|
|
184
|
-
function cloneItem(item) {
|
|
185
|
-
return JSON.parse(JSON.stringify(item));
|
|
186
|
-
}
|
|
187
|
-
function nextItemId(edit, base) {
|
|
188
153
|
const ids = new Set();
|
|
189
|
-
for (const track of
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
while (ids.has(`${base}-${serial}`))
|
|
196
|
-
serial++;
|
|
197
|
-
return `${base}-${serial}`;
|
|
154
|
+
for (const track of edit.tracks)
|
|
155
|
+
if ('items' in track)
|
|
156
|
+
for (const candidate of track.items)
|
|
157
|
+
collectIds(candidate, ids);
|
|
158
|
+
const items = (0, ripple_1.removeTimelineItemRange)(mediaItem, { start: mediaItem.at + startOffset, end: mediaItem.at + endOffset }, edit.output.fps, ids);
|
|
159
|
+
return { items, removedFrames: endOffset - startOffset };
|
|
198
160
|
}
|
|
199
161
|
function collectIds(item, ids) {
|
|
200
162
|
ids.add(item.id);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AudioFadeShape } from './envelope';
|
|
1
2
|
export type LaneV2 = 'visual' | 'audio';
|
|
2
3
|
export interface OutputV2 {
|
|
3
4
|
width: number;
|
|
@@ -429,6 +430,8 @@ export interface AudioMediaItemV2 {
|
|
|
429
430
|
keyframes?: KeyframeV2[];
|
|
430
431
|
fade_in?: number;
|
|
431
432
|
fade_out?: number;
|
|
433
|
+
fade_in_shape?: AudioFadeShape;
|
|
434
|
+
fade_out_shape?: AudioFadeShape;
|
|
432
435
|
ducking?: boolean;
|
|
433
436
|
duck_db?: number;
|
|
434
437
|
duck_attack?: number;
|
|
@@ -461,6 +464,8 @@ export interface VisualItemsTrackV2 {
|
|
|
461
464
|
name?: string;
|
|
462
465
|
/** トラックの音声をミュート。visual は cut の埋め込み音声、audio は item を書き出し・プレビューから除外。 */
|
|
463
466
|
muted?: boolean;
|
|
467
|
+
target?: boolean;
|
|
468
|
+
sync?: boolean;
|
|
464
469
|
items: ItemV2[];
|
|
465
470
|
}
|
|
466
471
|
export interface AudioItemsTrackV2 {
|
|
@@ -469,6 +474,8 @@ export interface AudioItemsTrackV2 {
|
|
|
469
474
|
name?: string;
|
|
470
475
|
/** トラックの音声をミュート。visual は cut の埋め込み音声、audio は item を書き出し・プレビューから除外。 */
|
|
471
476
|
muted?: boolean;
|
|
477
|
+
target?: boolean;
|
|
478
|
+
sync?: boolean;
|
|
472
479
|
items: AudioMediaItemV2[];
|
|
473
480
|
}
|
|
474
481
|
export type ItemsTrackV2 = VisualItemsTrackV2 | AudioItemsTrackV2;
|
|
@@ -478,6 +485,8 @@ export interface ContentTrackV2 {
|
|
|
478
485
|
name?: string;
|
|
479
486
|
/** トラックの音声をミュート。visual は cut の埋め込み音声、audio は item を書き出し・プレビューから除外。 */
|
|
480
487
|
muted?: boolean;
|
|
488
|
+
target?: boolean;
|
|
489
|
+
sync?: boolean;
|
|
481
490
|
content: CaptionTrackContentV2;
|
|
482
491
|
}
|
|
483
492
|
export type TrackV2 = ItemsTrackV2 | ContentTrackV2;
|
|
@@ -12,7 +12,7 @@ const ITEM_KEYS = new Set([
|
|
|
12
12
|
]);
|
|
13
13
|
const AUDIO_ITEM_KEYS = new Set([
|
|
14
14
|
'id', 'name', 'hidden', 'locked', 'at', 'duration', 'role', 'link', 'mute', 'source', 'gain_db', 'keyframes',
|
|
15
|
-
'fade_in', 'fade_out', 'ducking', 'duck_db', 'duck_attack', 'duck_release',
|
|
15
|
+
'fade_in', 'fade_out', 'fade_in_shape', 'fade_out_shape', 'ducking', 'duck_db', 'duck_attack', 'duck_release',
|
|
16
16
|
'denoise', 'lowcut_hz', 'script', 'reading', 'caption_ref', 'provenance', 'anchor'
|
|
17
17
|
]);
|
|
18
18
|
/**
|
|
@@ -121,7 +121,7 @@ function validateEditSource(value, index, ids) {
|
|
|
121
121
|
function validateTrack(value, index, trackIds, itemIds, sourceIds) {
|
|
122
122
|
const path = `edit.json.tracks[${index}]`;
|
|
123
123
|
requireRecord(value, path);
|
|
124
|
-
requireExactKeys(value, new Set(['id', 'lane', 'name', 'muted', 'items', 'content']), path);
|
|
124
|
+
requireExactKeys(value, new Set(['id', 'lane', 'name', 'muted', 'target', 'sync', 'items', 'content']), path);
|
|
125
125
|
requireText(value.id, `${path}.id`);
|
|
126
126
|
if (trackIds.has(value.id))
|
|
127
127
|
throw invalid(`${path}.id`, `track id が重複しています: ${value.id}`);
|
|
@@ -135,6 +135,11 @@ function validateTrack(value, index, trackIds, itemIds, sourceIds) {
|
|
|
135
135
|
if (hasOwn(value, 'muted') && typeof value.muted !== 'boolean') {
|
|
136
136
|
throw invalid(`${path}.muted`, 'boolean である必要があります');
|
|
137
137
|
}
|
|
138
|
+
for (const key of ['target', 'sync']) {
|
|
139
|
+
if (hasOwn(value, key) && typeof value[key] !== 'boolean') {
|
|
140
|
+
throw invalid(`${path}.${key}`, 'boolean である必要があります');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
138
143
|
const hasItems = hasOwn(value, 'items');
|
|
139
144
|
const hasContent = hasOwn(value, 'content');
|
|
140
145
|
if (hasItems === hasContent) {
|
|
@@ -190,6 +195,11 @@ function validateAudioItem(value, path, ids, sourceIds) {
|
|
|
190
195
|
requireNonNegativeNumber(value.fade_in, `${path}.fade_in`);
|
|
191
196
|
if (hasOwn(value, 'fade_out'))
|
|
192
197
|
requireNonNegativeNumber(value.fade_out, `${path}.fade_out`);
|
|
198
|
+
for (const field of ['fade_in_shape', 'fade_out_shape']) {
|
|
199
|
+
if (hasOwn(value, field) && !['linear', 'equal_power', 's_curve', 'slow'].includes(value[field])) {
|
|
200
|
+
throw invalid(`${path}.${field}`, 'linear/equal_power/s_curve/slow のいずれかである必要があります');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
193
203
|
if (hasOwn(value, 'ducking') && typeof value.ducking !== 'boolean') {
|
|
194
204
|
throw invalid(`${path}.ducking`, 'boolean である必要があります');
|
|
195
205
|
}
|
|
@@ -12,6 +12,16 @@ export interface EnvelopeGainEvent {
|
|
|
12
12
|
value: number;
|
|
13
13
|
method: 'set' | 'linear' | 'exponential';
|
|
14
14
|
}
|
|
15
|
+
export type AudioFadeShape = 'linear' | 'equal_power' | 's_curve' | 'slow';
|
|
16
|
+
/** The same progress curve is sampled by preview and named by the export filter. */
|
|
17
|
+
export declare function audioFadeProgress(shape: AudioFadeShape | undefined, progress: number): number;
|
|
18
|
+
export declare function audioFadeMultiplier(localSeconds: number, durationSeconds: number, fadeInSeconds: number, fadeOutSeconds: number, inShape?: AudioFadeShape, outShape?: AudioFadeShape): number;
|
|
19
|
+
/** Linear ramp events: 16 segments per active fade, including both endpoints. */
|
|
20
|
+
export declare function audioFadeGainEvents(durationSeconds: number, fadeInSeconds: number, fadeOutSeconds: number, inShape?: AudioFadeShape, outShape?: AudioFadeShape): EnvelopeGainEvent[];
|
|
21
|
+
/** afade's tri/qsin/hsin/qua correspond to p/sin(pπ/2)/(1−cos(pπ))/2/p². */
|
|
22
|
+
export declare function audioFadeFfmpegCurve(shape: AudioFadeShape | undefined): 'tri' | 'qsin' | 'hsin' | 'qua';
|
|
23
|
+
/** Carry v2-only shape declarations across the legacy audio projection without changing shape-less input. */
|
|
24
|
+
export declare function projectAudioFadeShapes<T extends Record<string, any>>(audio: T, tracks: unknown): T;
|
|
15
25
|
export declare const DEFAULT_DUCK_DB = -12;
|
|
16
26
|
export declare const DEFAULT_DUCK_ATTACK_SEC = 0.3;
|
|
17
27
|
export declare const DEFAULT_DUCK_RELEASE_SEC = 0.8;
|