akari-video 0.1.71 → 0.1.73
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/bin/akari.mjs +5 -1
- package/package.json +1 -1
- package/src/skills-command.mjs +160 -0
- package/vendor/.akari-capability-sources.json +1 -0
- package/vendor/docs/contract-2026-07-25-r6-audio-tracks-and-trim.md +123 -114
- package/vendor/docs/contract-2026-08-02-preview-parity.md +8 -0
- package/vendor/docs/contract-2026-08-03-caption-display-encoding-qc-v1.md +24 -3
- package/vendor/docs/contract-2026-09-13-generation-v0.md +56 -9
- package/vendor/docs/contract-2026-09-13-world-map-v0.md +4 -2
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/edit-lint/README.md +6 -0
- package/vendor/packages/edit-lint/src/edit-lint.mjs +125 -0
- package/vendor/packages/edit-store/lib/audio-schedule.js +31 -3
- package/vendor/packages/edit-store/lib/edit-v2-item-write.js +102 -10
- package/vendor/packages/edit-store/lib/internal-model.js +11 -0
- package/vendor/packages/edit-store/lib/webview-kernel.js +15 -3
- package/vendor/packages/edit-store/lib/write-gate.d.ts +22 -1
- package/vendor/packages/edit-store/lib/write-gate.js +81 -6
- package/vendor/packages/overlay-runtime/README.md +6 -1
- package/vendor/packages/schemas/engine-capabilities.json +2 -2
- package/vendor/skills/akari/SKILL.md +90 -0
- package/vendor/skills/akari/test/skills-command.test.mjs +2 -0
- package/vendor/skills/design-world/SKILL.md +3 -1
- package/vendor/skills/design-world/world.md +3 -0
- package/vendor/skills/overlay-authoring/3d.md +16 -5
package/bin/akari.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import { runDoctorCommand } from '../src/doctor-command.mjs';
|
|
|
16
16
|
import { runGenerateCommand } from '../src/generate-command.mjs';
|
|
17
17
|
import { runStoryboardCommand } from '../src/storyboard-command.mjs';
|
|
18
18
|
import { runWorldCommand } from '../src/world-command.mjs';
|
|
19
|
+
import { runSkillsCommand, refreshEntrySkillOnLaunch } from '../src/skills-command.mjs';
|
|
19
20
|
import { resolveRuntimePaths } from '../src/runtime-diagnostics.mjs';
|
|
20
21
|
import { maybeApplyPendingUpdateOnLaunch, resolveInstalledVersionInfo } from '../src/update-check.mjs';
|
|
21
22
|
import { describeCliHelp, describeInstalledVersions } from '../src/messages.mjs';
|
|
@@ -38,7 +39,8 @@ async function printVersion() {
|
|
|
38
39
|
// `--help` は claude/opencode へそのまま転送されてしまっていた — AKARI Video 自身の
|
|
39
40
|
// コマンド一覧が一度も出ない行き止まりだったため新設した)。
|
|
40
41
|
async function printCliHelp() {
|
|
41
|
-
for (const line of [...describeCliHelp(), ' world ワールド地図を検査・生成・プレビュー・停留所移動'
|
|
42
|
+
for (const line of [...describeCliHelp(), ' world ワールド地図を検査・生成・プレビュー・停留所移動',
|
|
43
|
+
' skills 入口スキルを配置・削除・確認(install/remove --entry, status --json)']) {
|
|
42
44
|
console.log(line);
|
|
43
45
|
}
|
|
44
46
|
return { exitCode: 0 };
|
|
@@ -60,6 +62,7 @@ try {
|
|
|
60
62
|
} catch (error) {
|
|
61
63
|
console.error(`自動更新の適用確認でエラーが発生しました(続行します): ${error instanceof Error ? error.message : String(error)}`);
|
|
62
64
|
}
|
|
65
|
+
refreshEntrySkillOnLaunch({ env: process.env });
|
|
63
66
|
|
|
64
67
|
// `akari update` / `akari init` / `akari new` / `akari narration` / `akari internal` /
|
|
65
68
|
// `akari sounds` / `akari status` / `akari accept` / `akari capability` / `akari store` /
|
|
@@ -73,6 +76,7 @@ const invoke = (argv[0] === '--version' || argv[0] === '-v') ? printVersion()
|
|
|
73
76
|
: argv[0] === 'update' ? runUpdateCommand(argv.slice(1))
|
|
74
77
|
: argv[0] === 'init' ? runInitCommand(argv.slice(1))
|
|
75
78
|
: argv[0] === 'new' ? runNewCommand(argv.slice(1))
|
|
79
|
+
: argv[0] === 'skills' ? runSkillsCommand(argv.slice(1))
|
|
76
80
|
: argv[0] === 'narration' ? runNarrationCommand(argv.slice(1))
|
|
77
81
|
: argv[0] === 'internal' ? runInternalCommand(argv.slice(1))
|
|
78
82
|
: argv[0] === 'sounds' ? runSoundsCommand(argv.slice(1))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.73",
|
|
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": {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { cpSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
7
|
+
export const ENTRY_VERSION_FILE = '.akari-entry-version';
|
|
8
|
+
const usage = `使い方: akari skills install --entry [--target <dir>]...
|
|
9
|
+
akari skills remove --entry [--target <dir>]...
|
|
10
|
+
akari skills status --json
|
|
11
|
+
--target は入口スキル自体の配置先(例: ~/.codex/skills/akari)。既定の 2 か所に追加する。`;
|
|
12
|
+
|
|
13
|
+
function stat(path) {
|
|
14
|
+
try { return lstatSync(path); } catch (error) {
|
|
15
|
+
if (error.code === 'ENOENT') return null;
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function canonicalBase(path) {
|
|
21
|
+
return stat(path) ? realpathSync(path) : resolve(path);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function entryTargets(env = process.env, extra = []) {
|
|
25
|
+
const home = canonicalBase(env.HOME || homedir());
|
|
26
|
+
return [...new Set([
|
|
27
|
+
join(home, '.claude', 'skills', 'akari'),
|
|
28
|
+
join(home, '.agents', 'skills', 'akari'),
|
|
29
|
+
...extra.map(path => resolve(path)),
|
|
30
|
+
])];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveEntrySource({ env = process.env, repoRoot = REPO_ROOT } = {}) {
|
|
34
|
+
const home = env.HOME || homedir();
|
|
35
|
+
for (const root of [join(env.AKARI_HOME || join(home, '.akari'), 'app'), repoRoot]) {
|
|
36
|
+
const source = join(root, 'skills', 'akari');
|
|
37
|
+
if (!stat(join(source, 'SKILL.md'))?.isFile()) continue;
|
|
38
|
+
const version = JSON.parse(readFileSync(join(root, 'packages', 'akari-launcher', 'package.json'), 'utf8')).version;
|
|
39
|
+
if (typeof version !== 'string' || !version.trim()) throw new Error(`ランチャー版が不正です: ${root}`);
|
|
40
|
+
return { source, version };
|
|
41
|
+
}
|
|
42
|
+
throw new Error('入口スキル skills/akari/SKILL.md が見つかりません。');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ディレクトリだけでなく配下の SKILL.md / 版印、親の skills がリンクの場合も触らない。
|
|
46
|
+
function hasLinkedAncestor(path) {
|
|
47
|
+
for (let current = path; ; current = dirname(current)) {
|
|
48
|
+
if (stat(current)?.isSymbolicLink()) return true;
|
|
49
|
+
if (dirname(current) === current) return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function hasLinkedContent(path) {
|
|
54
|
+
const info = stat(path);
|
|
55
|
+
if (!info) return false;
|
|
56
|
+
if (info.isSymbolicLink()) return true;
|
|
57
|
+
return info.isDirectory() && readdirSync(path).some(name => hasLinkedContent(join(path, name)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function inspectTarget(path, currentVersion) {
|
|
61
|
+
const info = stat(path);
|
|
62
|
+
const symlink = hasLinkedAncestor(path);
|
|
63
|
+
const marker = join(path, ENTRY_VERSION_FILE);
|
|
64
|
+
const managed = !symlink && info?.isDirectory() === true && stat(marker)?.isFile() === true;
|
|
65
|
+
const version = managed ? readFileSync(marker, 'utf8').trim() : null;
|
|
66
|
+
return { path, exists: info !== null, managed, symlink, version, stale: managed && version !== currentVersion };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function entryStatus(options = {}) {
|
|
70
|
+
const { version } = resolveEntrySource(options);
|
|
71
|
+
return { version, targets: entryTargets(options.env).map(path => inspectTarget(path, version)) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function installTarget(path, source, version, warn) {
|
|
75
|
+
if (hasLinkedAncestor(path) || hasLinkedContent(path) || hasLinkedContent(source)) {
|
|
76
|
+
warn(`symlink は変更しません: ${path}`);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const state = inspectTarget(path, version);
|
|
80
|
+
if (state.exists && !state.managed) {
|
|
81
|
+
warn(`入口スキルの版印がないため変更しません: ${path}`);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
mkdirSync(path, { recursive: true });
|
|
85
|
+
cpSync(source, path, {
|
|
86
|
+
recursive: true,
|
|
87
|
+
filter: entry => {
|
|
88
|
+
const relativePath = relative(source, entry);
|
|
89
|
+
if (!relativePath) return true;
|
|
90
|
+
const parts = relativePath.split(sep);
|
|
91
|
+
return parts[0] !== 'test' && !parts.some(part => part.startsWith('.'));
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
// コピーに失敗した場合は旧版印を維持して次の起動で再試行する。
|
|
95
|
+
writeFileSync(join(path, ENTRY_VERSION_FILE), `${version}\n`);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 明示 install 済みの既定先だけを更新する。起動を止めず、出力もしない。 */
|
|
100
|
+
export function refreshEntrySkillOnLaunch(options = {}) {
|
|
101
|
+
try {
|
|
102
|
+
const paths = entryTargets(options.env);
|
|
103
|
+
if (!paths.some(path => inspectTarget(path, null).managed)) return;
|
|
104
|
+
const { source, version } = resolveEntrySource(options);
|
|
105
|
+
for (const path of paths) {
|
|
106
|
+
try {
|
|
107
|
+
if (inspectTarget(path, version).stale) installTarget(path, source, version, () => {});
|
|
108
|
+
} catch { /* 次回起動で再試行 */ }
|
|
109
|
+
}
|
|
110
|
+
} catch { /* 入口の更新失敗で他のコマンドを止めない */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function runSkillsCommand(args, options = {}) {
|
|
114
|
+
const log = options.log ?? console.log;
|
|
115
|
+
const warn = options.logError ?? console.error;
|
|
116
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
117
|
+
log(usage);
|
|
118
|
+
return { exitCode: 0 };
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const [command, ...flags] = args;
|
|
122
|
+
const extra = [];
|
|
123
|
+
let entry = false;
|
|
124
|
+
let json = false;
|
|
125
|
+
for (let i = 0; i < flags.length; i++) {
|
|
126
|
+
if (flags[i] === '--entry') entry = true;
|
|
127
|
+
else if (flags[i] === '--json') json = true;
|
|
128
|
+
else if (flags[i] === '--target' && flags[i + 1] && !flags[i + 1].startsWith('-')) extra.push(flags[++i]);
|
|
129
|
+
else throw new Error(usage);
|
|
130
|
+
}
|
|
131
|
+
if (command === 'status' && json && !entry && !extra.length) {
|
|
132
|
+
log(JSON.stringify(entryStatus(options), null, 2));
|
|
133
|
+
return { exitCode: 0 };
|
|
134
|
+
}
|
|
135
|
+
if (!['install', 'remove'].includes(command) || !entry || json) throw new Error(usage);
|
|
136
|
+
const paths = entryTargets(options.env, extra);
|
|
137
|
+
const source = command === 'install' ? resolveEntrySource(options) : null;
|
|
138
|
+
let skipped = false;
|
|
139
|
+
for (const path of paths) {
|
|
140
|
+
if (command === 'install') {
|
|
141
|
+
if (installTarget(path, source.source, source.version, warn)) log(`入口スキルを配置しました: ${path}`);
|
|
142
|
+
else skipped = true;
|
|
143
|
+
} else {
|
|
144
|
+
if (!stat(path)) continue;
|
|
145
|
+
const state = inspectTarget(path, null);
|
|
146
|
+
if (!state.managed || hasLinkedContent(path)) {
|
|
147
|
+
warn(`版印がない、または symlink のため削除しません: ${path}`);
|
|
148
|
+
skipped = true;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
rmSync(path, { recursive: true });
|
|
152
|
+
log(`入口スキルを削除しました: ${path}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { exitCode: skipped ? 1 : 0 };
|
|
156
|
+
} catch (error) {
|
|
157
|
+
warn(error instanceof Error ? error.message : String(error));
|
|
158
|
+
return { exitCode: 1 };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -119,6 +119,7 @@
|
|
|
119
119
|
"packages/schemas/package.json",
|
|
120
120
|
"packages/word-book/package.json",
|
|
121
121
|
"skills/address-review/SKILL.md",
|
|
122
|
+
"skills/akari/SKILL.md",
|
|
122
123
|
"skills/analyze-footage/analysis-json.md",
|
|
123
124
|
"skills/analyze-footage/bin/face-expression/vendor/tasks-vision-0.10.17/README-AKARI.md",
|
|
124
125
|
"skills/analyze-footage/events-and-hooks.md",
|
|
@@ -1,114 +1,123 @@
|
|
|
1
|
-
# R6 契約 — タイムライン配置原則・音源複数トラック化・音源トリム・ソーストリマー
|
|
2
|
-
|
|
3
|
-
- 日付: 2026-07-25
|
|
4
|
-
- 状態: draft(裁定は確定。実装と並走で approved 化)。本書は技術仕様のみ。
|
|
5
|
-
判断経緯・実装レーンの運用は非公開の内部記録で管理する(本リポには置かない方針)
|
|
6
|
-
- 前提: `contract-2026-07-14-edit-json-v1-audio.md`(audio スキーマ正本)、
|
|
7
|
-
`contract-2026-07-17-data-contract-versioning.md`(三原則)
|
|
8
|
-
|
|
9
|
-
## 1. 確定事項(2026-07-25 裁定)
|
|
10
|
-
|
|
11
|
-
1. **タイムライン配置原則 = Premiere 型を正式採用**:
|
|
12
|
-
- 音源グループは**最下段固定**(並べ替え不可)
|
|
13
|
-
- cuts 帯(Video)はその上の縦中心。上に layers / captions(重ね物)
|
|
14
|
-
- 映像系トラック内の縦順は従来どおり**上の行ほど前面**(z 順裁定は不変)
|
|
15
|
-
- ルーラー(メモリ)位置は現状のまま固定
|
|
16
|
-
- 既定スタック(下から audio→cuts→layers→captions)と整合。本裁定はこれを
|
|
17
|
-
「固定の配置原則」として明文化するもの
|
|
18
|
-
2. **音源の重なり解消 = 複数音声トラック化**:
|
|
19
|
-
- sfx の `track` フィールド(schema 既存)を UI で解放し、音声もトラックを増やせるようにする
|
|
20
|
-
- 従来の「audio は当面 ref 0 固定(単一トラック)」運用を本裁定で変更
|
|
21
|
-
- `timelineTrack` は kind:'audio' の複数宣言を既に許容(schema 変更不要)。
|
|
22
|
-
音声トラック群は配置原則 1 により常に最下段グループ内で増減する
|
|
23
|
-
3. **ソーストリマーの入口 = タイムラインのクリップ dblclick**:
|
|
24
|
-
- クリップをダブルクリック → カット外部分を薄く表示し、左右スライドで in/out 調整
|
|
25
|
-
- 素材ファイルの dblclick = 素のソース再生、とは両立(入口が別)
|
|
26
|
-
|
|
27
|
-
## 2. 音源トリム(schema 拡張)
|
|
28
|
-
|
|
29
|
-
### schema
|
|
30
|
-
|
|
31
|
-
- `sfxItem` に optional `in` / `out` を追加(**素材秒**。`in` ≥ 0 省略時 0、
|
|
32
|
-
`out` > `in` 省略時 素材末尾)。再生区間 = 素材の [in, out)、
|
|
33
|
-
タイムライン上の開始は従来どおり `t`(timeline 秒)、表示尺 = out − in
|
|
34
|
-
- `narrationItem` にも同じ optional `in` / `out` を追加する。再生区間・素材秒・既定値・
|
|
35
|
-
`out > in` の検証分担は sfx と同一で、タイムライン上の開始は narration の `t` とする
|
|
36
|
-
- `bgm` に optional `in` を追加(BGM ファイル内の開始オフセット素材秒。ループ・全体尺
|
|
37
|
-
トリムの既存意味論は不変)
|
|
38
|
-
- edit-lint: `out <= in` を error。実尺越えの検知は lint では行わない
|
|
39
|
-
(lint は ffprobe を持たない — クランプは消費側の責務)
|
|
40
|
-
- cuts 側の語彙に倣い、$comment に意味論を明記する
|
|
41
|
-
|
|
42
|
-
### 消費(render-cut + preview)
|
|
43
|
-
|
|
44
|
-
- render-cut: sfx の [in, out) 切り出しを出力に反映。bgm の `in` オフセット反映
|
|
45
|
-
- render-cut: narration の [in, out) 切り出しも出力に反映する。`in` が素材実尺以上なら 0 へ、
|
|
46
|
-
`out` が素材実尺を超えれば素材末尾へクランプして warning を出す。クランプ後に `out <= in` なら
|
|
47
|
-
その narration 要素だけを skip する。`in` / `out` の有無にかかわらず、各 narration 要素は
|
|
48
|
-
デコード可否の判定を兼ねて実尺を従来と同じ 1 回だけ probe し、デコードできなければ従来どおり
|
|
49
|
-
その要素だけを skip して warning を出す一方、両方省略された要素には `atrim` を前置きせず、
|
|
50
|
-
従来とバイト同一のフィルタ文字列を保つ
|
|
51
|
-
- preview(previewAudio): 同意味論で再生。実尺越え in/out は素材末尾へクランプ
|
|
52
|
-
|
|
53
|
-
### UI
|
|
54
|
-
|
|
55
|
-
- 音源バーの端ドラッグでトリム(in/out 書き戻し)。動画クリップのトリムと同じ操作感
|
|
56
|
-
- 複数音声トラック行の表示・追加・アイテムのトラック間移動(裁定 2)
|
|
57
|
-
- 配置原則(裁定 1)の実装: audio グループ最下段固定・cuts 縦中心・上に重ね物。ルーラー無移動
|
|
58
|
-
|
|
59
|
-
## 3. ソーストリマー
|
|
60
|
-
|
|
61
|
-
- 入口: クリップ dblclick(裁定 3)。トリマーモード中はカット外を薄く表示し、
|
|
62
|
-
左右スライドで in/out を調整。解除は Esc / 再 dblclick / 他クリップ選択
|
|
63
|
-
- サムネイルは素材全体のフィルムストリップを 1 回だけ焼き、窓移動は CSS
|
|
64
|
-
background-position のみで行う(トリム / スリップ操作で再焼成しない設計)
|
|
65
|
-
|
|
66
|
-
## 4. 受け入れの軸
|
|
67
|
-
|
|
68
|
-
- schema: schemas / edit-lint テスト全数 green
|
|
69
|
-
- 消費: in/out 付き sfx の出力音声を ffprobe / 波形で実測(切り出し位置・尺一致)。
|
|
70
|
-
preview 側も同 fixture で聴感 + 実測。クランプ動作の実測
|
|
71
|
-
- UI: 実機で (a) 配置原則どおりの表示 (b) 音声トラック追加とアイテム移動が edit.json に
|
|
72
|
-
書き戻る (c) 音源バー端ドラッグで in/out 書き戻り・リロード後保持 (d) トリマーの
|
|
73
|
-
表示・調整が機能 (e) 既存トラック UI・z 順の無退行
|
|
74
|
-
|
|
75
|
-
## 5. §2 追記 — sfx フェード(audio-clip-fades, 2026-08-18・オーナー裁定「クリップ主義」T2)
|
|
76
|
-
|
|
77
|
-
BGM をクリップ化する裁定(内部リポ `akari-video-internal` の該当タスク)に伴い、
|
|
78
|
-
「音楽をクリップ(audio.sfx[])として置いても BGM ベッドと同じフェード表現ができる」を
|
|
79
|
-
満たすため、`sfxItem` に optional の `fade_in` / `fade_out`(秒・0 以上)を追加のみ拡張する
|
|
80
|
-
(`version` 不変・`contract-2026-07-17-data-contract-versioning.md` の原則に従う)。
|
|
81
|
-
|
|
82
|
-
### schema
|
|
83
|
-
|
|
84
|
-
- `sfxItem.fade_in` / `fade_out`: 秒・省略時 0(フェードなし)。`audio.bgm.fadeIn` /
|
|
85
|
-
`fadeOut`(camelCase)とは異なり **snake_case**(既存の `gain_db` と同じ命名系列)
|
|
86
|
-
- フェード対象はこのクリップの実効再生窓 `[t, t + 実効尺)`。実効尺は §2 の `[in, out)` が
|
|
87
|
-
既知なら `out − in`、`in`/`out` 省略時は素材尺(消費側が実尺を解決できた場合のみ)
|
|
88
|
-
- クランプ規則は `audio.bgm.fadeIn`/`fadeOut` と同型: `fade_in`/`fade_out` それぞれ独立に
|
|
89
|
-
実効尺の半分までクランプ(render-cut が実装、edit-lint は `in`/`out` が両方既知のときだけ
|
|
90
|
-
警告できる — lint は ffprobe を持たないため実尺越えの検知は消費側の責務、という §2 本文の
|
|
91
|
-
既存原則をフェードにもそのまま適用)
|
|
92
|
-
|
|
93
|
-
### 消費(render-cut + preview 3 面)
|
|
94
|
-
|
|
95
|
-
- render-cut: sfx の afade を volume の直後・adelay の直前に挿入する(adelay 後だと
|
|
96
|
-
`st=0` が delay 由来の無音区間を指してしまうため)。`in`/`out` 併用時は atrim/asetpts で
|
|
97
|
-
尺をリセットした後の実効尺基準で afade を計算する
|
|
98
|
-
- シェルプレビュー(akari-preview): sfx は 1 回きりの `BufferSourceNode` 再生のため、
|
|
99
|
-
bgm の毎 tick 再計算(fadeMultiplier)ではなく、schedule 時点で
|
|
100
|
-
`gain.gain.setValueAtTime`/`linearRampToValueAtTime` によるブレークポイント列を組む
|
|
101
|
-
(`sfxFadeGainSchedule`、シーク再開時は経過秒からブレークポイントを再構成)
|
|
102
|
-
-
|
|
103
|
-
`
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
1
|
+
# R6 契約 — タイムライン配置原則・音源複数トラック化・音源トリム・ソーストリマー
|
|
2
|
+
|
|
3
|
+
- 日付: 2026-07-25
|
|
4
|
+
- 状態: draft(裁定は確定。実装と並走で approved 化)。本書は技術仕様のみ。
|
|
5
|
+
判断経緯・実装レーンの運用は非公開の内部記録で管理する(本リポには置かない方針)
|
|
6
|
+
- 前提: `contract-2026-07-14-edit-json-v1-audio.md`(audio スキーマ正本)、
|
|
7
|
+
`contract-2026-07-17-data-contract-versioning.md`(三原則)
|
|
8
|
+
|
|
9
|
+
## 1. 確定事項(2026-07-25 裁定)
|
|
10
|
+
|
|
11
|
+
1. **タイムライン配置原則 = Premiere 型を正式採用**:
|
|
12
|
+
- 音源グループは**最下段固定**(並べ替え不可)
|
|
13
|
+
- cuts 帯(Video)はその上の縦中心。上に layers / captions(重ね物)
|
|
14
|
+
- 映像系トラック内の縦順は従来どおり**上の行ほど前面**(z 順裁定は不変)
|
|
15
|
+
- ルーラー(メモリ)位置は現状のまま固定
|
|
16
|
+
- 既定スタック(下から audio→cuts→layers→captions)と整合。本裁定はこれを
|
|
17
|
+
「固定の配置原則」として明文化するもの
|
|
18
|
+
2. **音源の重なり解消 = 複数音声トラック化**:
|
|
19
|
+
- sfx の `track` フィールド(schema 既存)を UI で解放し、音声もトラックを増やせるようにする
|
|
20
|
+
- 従来の「audio は当面 ref 0 固定(単一トラック)」運用を本裁定で変更
|
|
21
|
+
- `timelineTrack` は kind:'audio' の複数宣言を既に許容(schema 変更不要)。
|
|
22
|
+
音声トラック群は配置原則 1 により常に最下段グループ内で増減する
|
|
23
|
+
3. **ソーストリマーの入口 = タイムラインのクリップ dblclick**:
|
|
24
|
+
- クリップをダブルクリック → カット外部分を薄く表示し、左右スライドで in/out 調整
|
|
25
|
+
- 素材ファイルの dblclick = 素のソース再生、とは両立(入口が別)
|
|
26
|
+
|
|
27
|
+
## 2. 音源トリム(schema 拡張)
|
|
28
|
+
|
|
29
|
+
### schema
|
|
30
|
+
|
|
31
|
+
- `sfxItem` に optional `in` / `out` を追加(**素材秒**。`in` ≥ 0 省略時 0、
|
|
32
|
+
`out` > `in` 省略時 素材末尾)。再生区間 = 素材の [in, out)、
|
|
33
|
+
タイムライン上の開始は従来どおり `t`(timeline 秒)、表示尺 = out − in
|
|
34
|
+
- `narrationItem` にも同じ optional `in` / `out` を追加する。再生区間・素材秒・既定値・
|
|
35
|
+
`out > in` の検証分担は sfx と同一で、タイムライン上の開始は narration の `t` とする
|
|
36
|
+
- `bgm` に optional `in` を追加(BGM ファイル内の開始オフセット素材秒。ループ・全体尺
|
|
37
|
+
トリムの既存意味論は不変)
|
|
38
|
+
- edit-lint: `out <= in` を error。実尺越えの検知は lint では行わない
|
|
39
|
+
(lint は ffprobe を持たない — クランプは消費側の責務)
|
|
40
|
+
- cuts 側の語彙に倣い、$comment に意味論を明記する
|
|
41
|
+
|
|
42
|
+
### 消費(render-cut + preview)
|
|
43
|
+
|
|
44
|
+
- render-cut: sfx の [in, out) 切り出しを出力に反映。bgm の `in` オフセット反映
|
|
45
|
+
- render-cut: narration の [in, out) 切り出しも出力に反映する。`in` が素材実尺以上なら 0 へ、
|
|
46
|
+
`out` が素材実尺を超えれば素材末尾へクランプして warning を出す。クランプ後に `out <= in` なら
|
|
47
|
+
その narration 要素だけを skip する。`in` / `out` の有無にかかわらず、各 narration 要素は
|
|
48
|
+
デコード可否の判定を兼ねて実尺を従来と同じ 1 回だけ probe し、デコードできなければ従来どおり
|
|
49
|
+
その要素だけを skip して warning を出す一方、両方省略された要素には `atrim` を前置きせず、
|
|
50
|
+
従来とバイト同一のフィルタ文字列を保つ
|
|
51
|
+
- preview(previewAudio): 同意味論で再生。実尺越え in/out は素材末尾へクランプ
|
|
52
|
+
|
|
53
|
+
### UI
|
|
54
|
+
|
|
55
|
+
- 音源バーの端ドラッグでトリム(in/out 書き戻し)。動画クリップのトリムと同じ操作感
|
|
56
|
+
- 複数音声トラック行の表示・追加・アイテムのトラック間移動(裁定 2)
|
|
57
|
+
- 配置原則(裁定 1)の実装: audio グループ最下段固定・cuts 縦中心・上に重ね物。ルーラー無移動
|
|
58
|
+
|
|
59
|
+
## 3. ソーストリマー
|
|
60
|
+
|
|
61
|
+
- 入口: クリップ dblclick(裁定 3)。トリマーモード中はカット外を薄く表示し、
|
|
62
|
+
左右スライドで in/out を調整。解除は Esc / 再 dblclick / 他クリップ選択
|
|
63
|
+
- サムネイルは素材全体のフィルムストリップを 1 回だけ焼き、窓移動は CSS
|
|
64
|
+
background-position のみで行う(トリム / スリップ操作で再焼成しない設計)
|
|
65
|
+
|
|
66
|
+
## 4. 受け入れの軸
|
|
67
|
+
|
|
68
|
+
- schema: schemas / edit-lint テスト全数 green
|
|
69
|
+
- 消費: in/out 付き sfx の出力音声を ffprobe / 波形で実測(切り出し位置・尺一致)。
|
|
70
|
+
preview 側も同 fixture で聴感 + 実測。クランプ動作の実測
|
|
71
|
+
- UI: 実機で (a) 配置原則どおりの表示 (b) 音声トラック追加とアイテム移動が edit.json に
|
|
72
|
+
書き戻る (c) 音源バー端ドラッグで in/out 書き戻り・リロード後保持 (d) トリマーの
|
|
73
|
+
表示・調整が機能 (e) 既存トラック UI・z 順の無退行
|
|
74
|
+
|
|
75
|
+
## 5. §2 追記 — sfx フェード(audio-clip-fades, 2026-08-18・オーナー裁定「クリップ主義」T2)
|
|
76
|
+
|
|
77
|
+
BGM をクリップ化する裁定(内部リポ `akari-video-internal` の該当タスク)に伴い、
|
|
78
|
+
「音楽をクリップ(audio.sfx[])として置いても BGM ベッドと同じフェード表現ができる」を
|
|
79
|
+
満たすため、`sfxItem` に optional の `fade_in` / `fade_out`(秒・0 以上)を追加のみ拡張する
|
|
80
|
+
(`version` 不変・`contract-2026-07-17-data-contract-versioning.md` の原則に従う)。
|
|
81
|
+
|
|
82
|
+
### schema
|
|
83
|
+
|
|
84
|
+
- `sfxItem.fade_in` / `fade_out`: 秒・省略時 0(フェードなし)。`audio.bgm.fadeIn` /
|
|
85
|
+
`fadeOut`(camelCase)とは異なり **snake_case**(既存の `gain_db` と同じ命名系列)
|
|
86
|
+
- フェード対象はこのクリップの実効再生窓 `[t, t + 実効尺)`。実効尺は §2 の `[in, out)` が
|
|
87
|
+
既知なら `out − in`、`in`/`out` 省略時は素材尺(消費側が実尺を解決できた場合のみ)
|
|
88
|
+
- クランプ規則は `audio.bgm.fadeIn`/`fadeOut` と同型: `fade_in`/`fade_out` それぞれ独立に
|
|
89
|
+
実効尺の半分までクランプ(render-cut が実装、edit-lint は `in`/`out` が両方既知のときだけ
|
|
90
|
+
警告できる — lint は ffprobe を持たないため実尺越えの検知は消費側の責務、という §2 本文の
|
|
91
|
+
既存原則をフェードにもそのまま適用)
|
|
92
|
+
|
|
93
|
+
### 消費(render-cut + preview 3 面)
|
|
94
|
+
|
|
95
|
+
- render-cut: sfx の afade を volume の直後・adelay の直前に挿入する(adelay 後だと
|
|
96
|
+
`st=0` が delay 由来の無音区間を指してしまうため)。`in`/`out` 併用時は atrim/asetpts で
|
|
97
|
+
尺をリセットした後の実効尺基準で afade を計算する
|
|
98
|
+
- シェルプレビュー(akari-preview): sfx は 1 回きりの `BufferSourceNode` 再生のため、
|
|
99
|
+
bgm の毎 tick 再計算(fadeMultiplier)ではなく、schedule 時点で
|
|
100
|
+
`gain.gain.setValueAtTime`/`linearRampToValueAtTime` によるブレークポイント列を組む
|
|
101
|
+
(`sfxFadeGainSchedule`、シーク再開時は経過秒からブレークポイントを再構成)
|
|
102
|
+
- 同・会話音声(narration / 音声レーンの `role:'speech'`): 2026-09-18 追記。上と同じ
|
|
103
|
+
ブレークポイント列の仕組みに乗せる(`buildWebAudioSchedule` は kind に依らず
|
|
104
|
+
`fadeGainEvents` を通す)。**窓の取り方だけが sfx と非対称**で、sfx は item の実効尺を
|
|
105
|
+
そのまま使う一方、narration は `min(track.durationSec, max(0, duration − track.t))` と
|
|
106
|
+
タイムライン末尾で切る。これは `render-cut/src/plan.mjs` の実際の扱いに合わせたもので、
|
|
107
|
+
揃えると sfx が書き出しと食い違う。クランプ規則(実効尺の半分まで独立に)は共通
|
|
108
|
+
- なお cuts / layers の撮影素材音声(プレビューの kind `'speech'`)は宣言にフェード項目を
|
|
109
|
+
持たず、書き出し側も cut 音声に afade を掛けない。ここにフェードを足すと逆に
|
|
110
|
+
書き出しとの食い違いを作るため、**意図的に対象外**とする
|
|
111
|
+
- Web UI(preview-server): bgm と同じ毎 tick 再計算方式。ただしこの層は現状 sfx の
|
|
112
|
+
`in`/`out` トリム自体を未実装のため、フェードの実効尺は常にデコード済み素材全長を使う
|
|
113
|
+
(トリム実装時に合わせて見直す)
|
|
114
|
+
|
|
115
|
+
### インスペクター
|
|
116
|
+
|
|
117
|
+
- akari-annotations: sfx 選択時に bgm と同じ「フェード」タブ(`fadeIn`/`fadeOut` ノブ)を出す。
|
|
118
|
+
ducking は bgm 概念のため sfx には出さない
|
|
119
|
+
- 正本は `packages/edit-store`(edit.json テキスト手術)だが、本追記の実装レーン
|
|
120
|
+
(task 2026-08-18-audio-clip-fades)のファイル境界が `packages/edit-store` を含まないため、
|
|
121
|
+
書き戻りは `apps/shell/extensions/akari-annotations/src/common/sfx-fade-store.ts` に
|
|
122
|
+
境界内で完結する独立実装として置いた(`updateArrayElementByIndex` 等 edit-store の
|
|
123
|
+
export 済みユーティリティは再利用)。将来 edit-store 側の担当タスクが正本へ統合してよい
|
|
@@ -76,6 +76,14 @@ cut 境界の選択は宣言順ではなく解決済みタイムラインと z-o
|
|
|
76
76
|
語彙は `"source"` の 1 つだけで、マーカーが立つのは「今 fit 基準で描かれている全 media item に `scale × fit`
|
|
77
77
|
(`fit = min(outputW / srcW, outputH / srcH)`・srcW / srcH は表示回転後)を一度だけ焼き込んだ」ことを意味する
|
|
78
78
|
(部分適用は禁止。移行は `packages/edit-store/bin/normalize-geometry.mjs`)。x / y / rotate は両基準で同じ意味なので触らない。
|
|
79
|
+
**「ソース実寸」= 原本の論理寸法(表示回転後)であり、復号フレームの画素寸法ではない(2026-09-18 追記)**。
|
|
80
|
+
プロキシを復号していてもこの基準は動かない。ここが曖昧だったため、ベースカットのクロップ計算が
|
|
81
|
+
復号フレーム=プロキシ寸法を使い、追加レイヤーと寸法基準が食い違っていた(不具合メモ 第10項:
|
|
82
|
+
1920×1080 原本 / 960×540 プロキシで crop 幅 0.5・scale 1 が 960×1080 ではなく 480×540 になる)。
|
|
83
|
+
frame-engine 側は `NativeFrameSource.logicalSize` の宣言を基準に使い、宣言が無い / 壊れている
|
|
84
|
+
ときだけ復号寸法へ退避する(`compositionSourceSize`)。したがってプロキシを復号し得る呼び出し側は
|
|
85
|
+
原本メタデータから `logicalSize` を宣言する責務を負う。宣言を足すのと、プロキシ差し替えに伴う
|
|
86
|
+
倍率補償を外すのは、二重補正を避けるため**同一の作業単位**で行う。
|
|
79
87
|
**G1(マーカー・移行・lint の warning `geometry.fit-compat`)ではエンジンはこのマーカーを読まず、描画は 1 バイトも変わらない。
|
|
80
88
|
描画へ反映するのは G2**。cross ref: `docs/contract-2026-07-22-render-basics.md` §4-1(#6 画角操作)。
|
|
81
89
|
|
|
@@ -10,9 +10,30 @@
|
|
|
10
10
|
`packages/edit-store/src/caption-display.ts` is the only resolver for the opt-in
|
|
11
11
|
`display_policy.mode: "single_line_sequential"` contract. It projects source captions through a
|
|
12
12
|
linear cut/speed/multi-source timeline, then resolves one or two fragments, timing, source
|
|
13
|
-
provenance, merged style variables, and reference-pixel geometry.
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
provenance, merged style variables, and reference-pixel geometry. Browser code only selects
|
|
14
|
+
already-resolved timeline cues; it must not call `Intl.Segmenter` or implement the split algorithm.
|
|
15
|
+
|
|
16
|
+
Every consumer reaches that kernel through one shared entry point,
|
|
17
|
+
`packages/render-cut/src/caption-resolve.mjs` (`resolveCaptionPlan`), which also owns the steps in
|
|
18
|
+
front of it: style-preset resolution, excluded-cue filtering, word-book protected terms, and cut
|
|
19
|
+
normalization. The consumers are render-cut's internal render path, preview-server, gpu-export,
|
|
20
|
+
and osr-export. Naming the kernel alone was not enough: until 2026-09-20 each consumer assembled
|
|
21
|
+
that front half itself, so preview-server handed the kernel a v2 edit with no derived `cuts` and
|
|
22
|
+
resolved zero display cues, while gpu-export and osr-export never consulted `display_policy` at all
|
|
23
|
+
and re-split captions through the legacy overlay generator. A consumer that calls the kernel
|
|
24
|
+
directly, or rebuilds any of those front-half steps, is a deviation.
|
|
25
|
+
|
|
26
|
+
**Known remaining deviation — the shell backend.** `AkariPreviewService.resolveCaptionDisplay`
|
|
27
|
+
(`apps/shell/extensions/akari-preview/src/node/akari-preview-service.ts`) still calls the kernel
|
|
28
|
+
directly and rebuilds the front half on its own: its own preset resolution, its own cut
|
|
29
|
+
normalization (`captionCompatibleCuts`, computed in frames off `internal.tracks` rather than the
|
|
30
|
+
shared `captionDisplayEdit`), and its own word-book lookup that walks up from `__dirname` and
|
|
31
|
+
silently degrades to no protected terms on any failure. **It never applies excluded-cue
|
|
32
|
+
filtering**, so a cue excluded through `tracks[].items[].source.exclude` still appears in the
|
|
33
|
+
in-app preview while the other four paths drop it. This is a preview-parity hazard of the same
|
|
34
|
+
class as the two defects above. It is listed here rather than silently tolerated; the shell is
|
|
35
|
+
bundled by Theia and cannot assume `packages/` sits next to it, so routing it through
|
|
36
|
+
`resolveCaptionPlan` is a packaging question, not a one-line import.
|
|
16
37
|
|
|
17
38
|
The policy rejects unsupported `at`, `track`, transition, and timeline winner semantics; caption
|
|
18
39
|
style/emphasis conflicts; non-NFC or trimmed text; invalid manual fragments; unresolved long text;
|