akari-video 0.1.18 → 0.1.20
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/cli.mjs +14 -7
- package/src/messages.mjs +11 -1
- package/vendor/docs/contract-2026-07-22-render-basics.md +17 -0
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/edit-lint/src/edit-lint.mjs +65 -2
- package/vendor/packages/edit-store/lib/cut-adjacency.d.ts +25 -0
- package/vendor/packages/edit-store/lib/cut-adjacency.js +48 -5
- package/vendor/packages/edit-store/lib/edit-store.d.ts +21 -0
- package/vendor/packages/edit-store/lib/edit-store.js +100 -0
- package/vendor/packages/edit-store/lib/internal-model.d.ts +13 -0
- package/vendor/packages/edit-store/lib/internal-model.js +36 -18
- package/vendor/packages/project-scaffold/src/index.mjs +3 -0
- package/vendor/packages/project-scaffold/test/create-project.test.mjs +8 -0
- package/vendor/skills/create-project/SKILL.md +21 -0
- package/vendor/templates/project-default/AGENTS.md +2 -0
- package/vendor/templates/project-default/CLAUDE.md +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
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/cli.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { resolveLauncherAssets } from './repo-assets.mjs';
|
|
|
6
6
|
import { detectProjectState } from './project-state.mjs';
|
|
7
7
|
import { findClaudeExecutable, findOpencodeExecutable } from './path-lookup.mjs';
|
|
8
8
|
import { loadTaskLabels } from './task-labels.mjs';
|
|
9
|
-
import { describeForceReinstall, describeInstalledVersions, describeIntake, claudeMissingGuidance, opencodeMissingGuidance, describeUpdateCommand, describeVersionStatus, formatUpdateNotice } from './messages.mjs';
|
|
9
|
+
import { describeForceReinstall, describeInstalledVersions, describeIntake, claudeMissingGuidance, opencodeMissingGuidance, describeUpdateCacheFallback, describeUpdateCommand, describeVersionStatus, formatUpdateNotice } from './messages.mjs';
|
|
10
10
|
import { resolveEffectiveProjectRoot } from './first-run.mjs';
|
|
11
11
|
import { maybeShowAssetIntroNotice } from './sounds-setup.mjs';
|
|
12
12
|
import {
|
|
@@ -188,8 +188,8 @@ function defaultSpawnOpencode(opencodePath, args, projectRoot) {
|
|
|
188
188
|
* 自己更新を試みず、キャッシュに載っている最新版の通知を今後出さないよう記録するだけ
|
|
189
189
|
* (既存挙動を維持)。`--force` は同じ版の本体も再導入し、`--rollback` は直前 1 世代
|
|
190
190
|
* (`~/.akari/app-previous/`)へ戻す。
|
|
191
|
-
*
|
|
192
|
-
*
|
|
191
|
+
* `--dismiss` / `--rollback` 以外の明示 update は、キャッシュ TTL に関係なく
|
|
192
|
+
* フィードの同期取得を 1 回試す。取得失敗時だけ既存キャッシュへフォールバックする。
|
|
193
193
|
*/
|
|
194
194
|
export async function runUpdateCommand(args, options = {}) {
|
|
195
195
|
const log = options.log ?? ((line) => console.log(line));
|
|
@@ -201,6 +201,7 @@ export async function runUpdateCommand(args, options = {}) {
|
|
|
201
201
|
const dismissRequested = args.includes('--dismiss');
|
|
202
202
|
const rollbackRequested = args.includes('--rollback');
|
|
203
203
|
const forceRequested = args.includes('--force');
|
|
204
|
+
let usingCachedFeed = false;
|
|
204
205
|
|
|
205
206
|
if (rollbackRequested) {
|
|
206
207
|
return (options.rollbackSelfUpdate ?? rollbackSelfUpdate)({ env, log });
|
|
@@ -219,9 +220,12 @@ export async function runUpdateCommand(args, options = {}) {
|
|
|
219
220
|
return { exitCode: 0 };
|
|
220
221
|
}
|
|
221
222
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
cache =
|
|
223
|
+
const refreshed = await (options.refreshUpdateFeed ?? refreshUpdateFeed)({ env, fetchImpl: options.fetchImpl });
|
|
224
|
+
if (isValidFeedShape(refreshed?.feed)) {
|
|
225
|
+
cache = refreshed;
|
|
226
|
+
} else {
|
|
227
|
+
cache = readCacheSync(cachePath) ?? cache;
|
|
228
|
+
usingCachedFeed = isValidFeedShape(cache?.feed);
|
|
225
229
|
}
|
|
226
230
|
|
|
227
231
|
const feed = cache?.feed;
|
|
@@ -234,12 +238,15 @@ export async function runUpdateCommand(args, options = {}) {
|
|
|
234
238
|
&& (hasManagedApp || (options.isRunningFromAppDir ?? isRunningFromAppDir)({ env, launcherRoot: options.launcherRoot }));
|
|
235
239
|
|
|
236
240
|
if (!selfUpdateEligible) {
|
|
237
|
-
for (const line of describeUpdateCommand({ currentVersion, versionInfo, cache, dismissed: false })) {
|
|
241
|
+
for (const line of describeUpdateCommand({ currentVersion, versionInfo, cache, dismissed: false, usingCachedFeed })) {
|
|
238
242
|
log(line);
|
|
239
243
|
}
|
|
240
244
|
return { exitCode: 0 };
|
|
241
245
|
}
|
|
242
246
|
|
|
247
|
+
if (usingCachedFeed) {
|
|
248
|
+
log(describeUpdateCacheFallback(cache));
|
|
249
|
+
}
|
|
243
250
|
for (const line of describeInstalledVersions(versionInfo)) {
|
|
244
251
|
log(line);
|
|
245
252
|
}
|
package/src/messages.mjs
CHANGED
|
@@ -172,7 +172,7 @@ export function creatorRootPromptText(defaultPath) {
|
|
|
172
172
|
* `akari update` の出力本文(複数行)。フィード未取得・最新・新版ありで案内が変わる。
|
|
173
173
|
* `dismissed` は今回の実行で dismiss 記録を書いたかどうか(表示文言の切り替えのみに使う)。
|
|
174
174
|
*/
|
|
175
|
-
export function describeUpdateCommand({ currentVersion, versionInfo, cache, dismissed }) {
|
|
175
|
+
export function describeUpdateCommand({ currentVersion, versionInfo, cache, dismissed, usingCachedFeed = false }) {
|
|
176
176
|
const info = versionInfo ?? normalizeVersionInfo(currentVersion);
|
|
177
177
|
const lines = versionInfo ? describeInstalledVersions(info) : [`現在のバージョン: v${currentVersion}`];
|
|
178
178
|
const feed = cache?.feed;
|
|
@@ -182,6 +182,10 @@ export function describeUpdateCommand({ currentVersion, versionInfo, cache, dism
|
|
|
182
182
|
return lines;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
if (usingCachedFeed) {
|
|
186
|
+
lines.push(describeUpdateCacheFallback(cache));
|
|
187
|
+
}
|
|
188
|
+
|
|
185
189
|
lines.push(`最新バージョン: v${feed.product}${channelSuffix(feed.channel)}`);
|
|
186
190
|
if (feed.notes_url) {
|
|
187
191
|
lines.push(`リリースノート: ${feed.notes_url}`);
|
|
@@ -208,6 +212,12 @@ export function describeUpdateCommand({ currentVersion, versionInfo, cache, dism
|
|
|
208
212
|
return lines;
|
|
209
213
|
}
|
|
210
214
|
|
|
215
|
+
/** 明示 update の再取得失敗時に、参照するキャッシュの取得時刻を示す 1 行。 */
|
|
216
|
+
export function describeUpdateCacheFallback(cache) {
|
|
217
|
+
const fetchedAt = typeof cache?.fetched_at === 'string' ? cache.fetched_at : '取得時刻不明';
|
|
218
|
+
return `更新フィードを取得できなかったため、${fetchedAt} 時点のキャッシュを表示します。`;
|
|
219
|
+
}
|
|
220
|
+
|
|
211
221
|
/**
|
|
212
222
|
* `akari init` の出力文言(タスク契約 launcher-init・内部リポ)。作業場
|
|
213
223
|
* (creator-root)の作成・確認だけを行う入口コマンド専用。1 行主義の既存流儀に従う。
|
|
@@ -113,6 +113,23 @@ perspective のレイヤー分割フォールバック — perspective は `crop
|
|
|
113
113
|
外形を四隅の基準にし、rotate と非 normal blend は固定外形化による既存意味の変化を排除できない
|
|
114
114
|
ため、この組み合わせだけは従来互換経路を維持する。
|
|
115
115
|
|
|
116
|
+
- **主映像カット拡大・crop の固定キャンバス化(2026-08-25 追記)**: v2 主映像トラックから
|
|
117
|
+
gap-aware cuts へ変換された `transform.scale` / crop キーフレームにも、レイヤー経路と同じ
|
|
118
|
+
`layerFixedCanvasKeyframeSteps` を適用する。normal blend・perspective 非宣言・rotate=0(静的・
|
|
119
|
+
キーフレームとも)の場合に限り、素材ネイティブ寸法の 2 倍グリッドで補間し、最大 footprint の
|
|
120
|
+
固定透明キャンバスへ偶数座標で配置した後、Lanczos で実寸へ縮小する。これにより毎フレームの
|
|
121
|
+
可変 bitmap 寸法と中央 overlay の組み合わせを排除し、拡大後サイズの偶奇変化による ±1px 往復を
|
|
122
|
+
防ぐ。perspective・rotate・非 normal blend は従来互換経路を維持し、キーフレーム無しカットの
|
|
123
|
+
filter 文字列は変更しない。
|
|
124
|
+
|
|
125
|
+
- **rotate 適用条件の追補(2026-08-25)**: 上記の「rotate=0(静的・キーフレームとも)」は、
|
|
126
|
+
下流 filter に rotate ステップが実際に出るかどうかへ読み替え、`rotateConstant === 0` のときだけ
|
|
127
|
+
固定キャンバスを適用する。静的 `transform.rotate` が非 0 でも、キーフレームが `transform` を
|
|
128
|
+
宣言すれば rotate はキーフレーム由来の既定 0 で解決され、rotate ステップが出ないため対象となる
|
|
129
|
+
(rotate の描画意味論は不変)。これで `layers.mjs` の同名ガードと一致する。静的 rotate 非 0 かつ
|
|
130
|
+
transform キーフレーム無し、キーフレーム rotate が非 0、rotate が時間変化する構成は、rotate
|
|
131
|
+
ステップが実際に出るため従来互換経路を維持する。
|
|
132
|
+
|
|
116
133
|
## 5. macOS の字幕レンダ用 Chrome 起動(2026-08-24 追記)
|
|
117
134
|
|
|
118
135
|
- macOS では `.app` 内の Chrome 実行ファイルを子プロセスとして直接起動しない。書き出し専用の
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
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": [
|
|
@@ -20,6 +20,8 @@ import { resolveFfmpeg, resolveFfprobe } from "../../media-bin/src/index.mjs";
|
|
|
20
20
|
|
|
21
21
|
const {
|
|
22
22
|
areCutsAdjacent,
|
|
23
|
+
cutOverlapFrames,
|
|
24
|
+
findCrossTrackLayerEvacuations,
|
|
23
25
|
findUnsupportedDeclaredTrackTransitions,
|
|
24
26
|
projectLegacyEdit,
|
|
25
27
|
readInternalEdit,
|
|
@@ -135,6 +137,7 @@ export async function lintProject(input, options = {}) {
|
|
|
135
137
|
const rawEdit = edit;
|
|
136
138
|
const internalEdit = readInternalEdit(rawEdit);
|
|
137
139
|
const legacyEdit = projectLegacyEdit(internalEdit);
|
|
140
|
+
validateTransitionLayerEvacuations(rawEdit, internalEdit, findings);
|
|
138
141
|
const rawAudio = isRecord(rawEdit.audio) ? rawEdit.audio : {};
|
|
139
142
|
const projectedAudio = projectAudioForLint(internalEdit);
|
|
140
143
|
edit = {
|
|
@@ -325,8 +328,24 @@ function validateTransitionAdjacency(cuts, segments, fps, findings) {
|
|
|
325
328
|
const transition = cuts?.[earlier.index]?.transition_out;
|
|
326
329
|
if (!isRecord(transition) || !isPositiveNumber(transition.duration)) continue;
|
|
327
330
|
const later = segments.slice(position + 1).find(candidate => candidate.track === earlier.track);
|
|
328
|
-
if (!later
|
|
329
|
-
|
|
331
|
+
if (!later) continue;
|
|
332
|
+
const overlapFrames = cutOverlapFrames(
|
|
333
|
+
{ tlEnd: earlier.end },
|
|
334
|
+
{ tlStart: later.start },
|
|
335
|
+
fps,
|
|
336
|
+
);
|
|
337
|
+
if (overlapFrames === 0) {
|
|
338
|
+
addFinding(findings, {
|
|
339
|
+
severity: "warning",
|
|
340
|
+
check: "cuts.transition-out.zero-overlap",
|
|
341
|
+
message: "トランジションを宣言していますが、次のクリップと重なっていないため効きません。前のクリップの終わりを延ばして重なりを作るか、トランジションを削除してください。",
|
|
342
|
+
path: `edit.json#cuts[${earlier.index}].transition_out`,
|
|
343
|
+
range: { start: earlier.end, end: later.start },
|
|
344
|
+
});
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
// 正の重なりは、宣言尺未満なら既存の短縮クランプ、宣言尺超なら track-overlap が担当する。
|
|
348
|
+
if (overlapFrames > 0 || areCutsAdjacent(
|
|
330
349
|
{ tlEnd: earlier.end, transitionOut: { duration: transition.duration } },
|
|
331
350
|
{ tlStart: later.start },
|
|
332
351
|
fps,
|
|
@@ -341,6 +360,50 @@ function validateTransitionAdjacency(cuts, segments, fps, findings) {
|
|
|
341
360
|
}
|
|
342
361
|
}
|
|
343
362
|
|
|
363
|
+
function validateTransitionLayerEvacuations(rawEdit, internalEdit, findings) {
|
|
364
|
+
if (!isRecord(rawEdit) || rawEdit.version !== 2) return;
|
|
365
|
+
const crossTrackCauses = new Map();
|
|
366
|
+
for (const cause of findCrossTrackLayerEvacuations(rawEdit)) {
|
|
367
|
+
if (!crossTrackCauses.has(cause.itemId)) crossTrackCauses.set(cause.itemId, cause);
|
|
368
|
+
}
|
|
369
|
+
const rawLocations = new Map();
|
|
370
|
+
if (Array.isArray(rawEdit.tracks)) {
|
|
371
|
+
rawEdit.tracks.forEach((track, trackIndex) => {
|
|
372
|
+
if (!isRecord(track) || !Array.isArray(track.items)) return;
|
|
373
|
+
track.items.forEach((item, itemIndex) => {
|
|
374
|
+
if (isRecord(item) && typeof item.id === "string") {
|
|
375
|
+
rawLocations.set(item.id, { trackIndex, itemIndex });
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
for (const track of internalEdit.tracks) {
|
|
381
|
+
for (const item of track.items) {
|
|
382
|
+
const transition = item.declaration?.transition_out;
|
|
383
|
+
if (item.legacy.collection !== "layers" || !isRecord(transition)) continue;
|
|
384
|
+
const location = rawLocations.get(item.id);
|
|
385
|
+
const cause = crossTrackCauses.get(item.id);
|
|
386
|
+
const reason = cause
|
|
387
|
+
? `このクリップは他トラックのアイテム(${cause.causeItemId})と重なっているため PiP 経路へ退避され、宣言したトランジションは書き出されません。`
|
|
388
|
+
: "このクリップは合成機能または同一トラック内の重なりにより PiP 経路へ退避され、宣言したトランジションは書き出されません。";
|
|
389
|
+
addFinding(findings, {
|
|
390
|
+
severity: "warning",
|
|
391
|
+
check: "cuts.transition-out.layer-evacuated",
|
|
392
|
+
message: `${reason}重なりを解消するか、トランジションを削除してください。`,
|
|
393
|
+
path: location
|
|
394
|
+
? `edit.json#tracks[${location.trackIndex}].items[${location.itemIndex}].source.transition_out`
|
|
395
|
+
: `edit.json#tracks[${track.z}].items`,
|
|
396
|
+
...(cause ? {
|
|
397
|
+
range: {
|
|
398
|
+
start: cause.overlapStartFrames / internalEdit.output.fps,
|
|
399
|
+
end: cause.overlapEndFrames / internalEdit.output.fps,
|
|
400
|
+
},
|
|
401
|
+
} : {}),
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
344
407
|
function projectAudioForLint(internalEdit) {
|
|
345
408
|
const sfx = [];
|
|
346
409
|
const narration = [];
|
|
@@ -12,6 +12,31 @@ export interface CutAdjacencyEarlierLike {
|
|
|
12
12
|
export interface CutAdjacencyLaterLike {
|
|
13
13
|
tlStart: number;
|
|
14
14
|
}
|
|
15
|
+
export declare function effectiveCutFps(fps: number): number;
|
|
16
|
+
/**
|
|
17
|
+
* earlier の終端と later の開始を出力 fps へ量子化し、実重なりをフレーム数で返す。
|
|
18
|
+
* 正数は重なり、0 は突き合わせ、負数はすき間を表す。
|
|
19
|
+
*/
|
|
20
|
+
export declare function cutOverlapFrames(earlier: Pick<CutAdjacencyEarlierLike, 'tlEnd'>, later: CutAdjacencyLaterLike, fps?: number): number;
|
|
21
|
+
export type TransitionHandleExtensionOutcome = 'already-overlapping' | 'full' | 'partial' | 'none';
|
|
22
|
+
export interface TransitionHandleExtensionPlan {
|
|
23
|
+
appliedSeconds: number;
|
|
24
|
+
effectiveSeconds: number;
|
|
25
|
+
appliedFrames: number;
|
|
26
|
+
outcome: TransitionHandleExtensionOutcome;
|
|
27
|
+
}
|
|
28
|
+
export interface TransitionHandleExtensionInput {
|
|
29
|
+
declaredSeconds: number;
|
|
30
|
+
earlierEndSeconds: number;
|
|
31
|
+
laterStartSeconds: number;
|
|
32
|
+
maxExtendSeconds: number;
|
|
33
|
+
fps?: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 突き合わせ境界へ宣言尺ぶんの重なりを作るため、outgoing を何フレーム延ばすか決める。
|
|
37
|
+
* メディア長は知らず、呼び出し側が maxExtendSeconds として渡した上限だけを使う。
|
|
38
|
+
*/
|
|
39
|
+
export declare function planTransitionHandleExtension(input: TransitionHandleExtensionInput): TransitionHandleExtensionPlan;
|
|
15
40
|
/**
|
|
16
41
|
* ギャップ 0、または earlier の宣言済み transitionOut.duration で説明できる重なりだけを
|
|
17
42
|
* 隣接とみなす。秒の誤差ではなく、出力 fps で量子化したフレーム数を比較する。
|
|
@@ -4,20 +4,63 @@
|
|
|
4
4
|
* タイムライン UI と edit-lint は必ずこの関数を使い、フレーム量子化の条件式を複製しない。
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.effectiveCutFps = effectiveCutFps;
|
|
8
|
+
exports.cutOverlapFrames = cutOverlapFrames;
|
|
9
|
+
exports.planTransitionHandleExtension = planTransitionHandleExtension;
|
|
7
10
|
exports.areCutsAdjacent = areCutsAdjacent;
|
|
8
11
|
const DEFAULT_CUT_ADJACENCY_FPS = 30;
|
|
9
|
-
function
|
|
12
|
+
function effectiveCutFps(fps) {
|
|
10
13
|
return Number.isFinite(fps) && fps > 0 ? fps : DEFAULT_CUT_ADJACENCY_FPS;
|
|
11
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* earlier の終端と later の開始を出力 fps へ量子化し、実重なりをフレーム数で返す。
|
|
17
|
+
* 正数は重なり、0 は突き合わせ、負数はすき間を表す。
|
|
18
|
+
*/
|
|
19
|
+
function cutOverlapFrames(earlier, later, fps = DEFAULT_CUT_ADJACENCY_FPS) {
|
|
20
|
+
const resolvedFps = effectiveCutFps(fps);
|
|
21
|
+
return Math.round(earlier.tlEnd * resolvedFps) - Math.round(later.tlStart * resolvedFps);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 突き合わせ境界へ宣言尺ぶんの重なりを作るため、outgoing を何フレーム延ばすか決める。
|
|
25
|
+
* メディア長は知らず、呼び出し側が maxExtendSeconds として渡した上限だけを使う。
|
|
26
|
+
*/
|
|
27
|
+
function planTransitionHandleExtension(input) {
|
|
28
|
+
const fps = effectiveCutFps(input.fps ?? DEFAULT_CUT_ADJACENCY_FPS);
|
|
29
|
+
const declaredFrames = Number.isFinite(input.declaredSeconds) && input.declaredSeconds > 0
|
|
30
|
+
? Math.max(1, Math.round(input.declaredSeconds * fps)) : 0;
|
|
31
|
+
const overlapFrames = cutOverlapFrames({ tlEnd: input.earlierEndSeconds }, { tlStart: input.laterStartSeconds }, fps);
|
|
32
|
+
if (overlapFrames > 0) {
|
|
33
|
+
return {
|
|
34
|
+
appliedSeconds: 0,
|
|
35
|
+
effectiveSeconds: Math.min(declaredFrames, overlapFrames) / fps,
|
|
36
|
+
appliedFrames: 0,
|
|
37
|
+
outcome: 'already-overlapping'
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const maximumFrames = input.maxExtendSeconds === Number.POSITIVE_INFINITY
|
|
41
|
+
? Number.POSITIVE_INFINITY
|
|
42
|
+
: Number.isFinite(input.maxExtendSeconds) && input.maxExtendSeconds > 0
|
|
43
|
+
? Math.max(0, Math.floor(input.maxExtendSeconds * fps + 1e-9)) : 0;
|
|
44
|
+
// すき間(負の overlapFrames)がある場合は、その穴を埋めたうえで宣言尺ぶん重ねる。
|
|
45
|
+
// UI は非隣接ガードでこの経路へ入れないが、純関数としては安全な値を返す。
|
|
46
|
+
const requiredFrames = Math.max(0, declaredFrames - overlapFrames);
|
|
47
|
+
const appliedFrames = Math.min(requiredFrames, maximumFrames);
|
|
48
|
+
const effectiveFrames = Math.max(0, Math.min(declaredFrames, overlapFrames + appliedFrames));
|
|
49
|
+
return {
|
|
50
|
+
appliedSeconds: appliedFrames / fps,
|
|
51
|
+
effectiveSeconds: effectiveFrames / fps,
|
|
52
|
+
appliedFrames,
|
|
53
|
+
outcome: appliedFrames <= 0 ? 'none'
|
|
54
|
+
: appliedFrames >= requiredFrames ? 'full' : 'partial'
|
|
55
|
+
};
|
|
56
|
+
}
|
|
12
57
|
/**
|
|
13
58
|
* ギャップ 0、または earlier の宣言済み transitionOut.duration で説明できる重なりだけを
|
|
14
59
|
* 隣接とみなす。秒の誤差ではなく、出力 fps で量子化したフレーム数を比較する。
|
|
15
60
|
*/
|
|
16
61
|
function areCutsAdjacent(earlier, later, fps = DEFAULT_CUT_ADJACENCY_FPS) {
|
|
17
|
-
const resolvedFps =
|
|
18
|
-
const
|
|
19
|
-
const laterStartFrame = Math.round(later.tlStart * resolvedFps);
|
|
20
|
-
const overlapFrames = earlierEndFrame - laterStartFrame;
|
|
62
|
+
const resolvedFps = effectiveCutFps(fps);
|
|
63
|
+
const overlapFrames = cutOverlapFrames(earlier, later, resolvedFps);
|
|
21
64
|
const declaredDuration = earlier.transitionOut?.duration;
|
|
22
65
|
const declaredOverlapFrames = typeof declaredDuration === 'number'
|
|
23
66
|
&& Number.isFinite(declaredDuration) && declaredDuration > 0
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TransitionHandleExtensionPlan } from './cut-adjacency';
|
|
1
2
|
export interface EditCut {
|
|
2
3
|
in: number;
|
|
3
4
|
out: number;
|
|
@@ -143,6 +144,26 @@ export declare function setCutTransitionOutInSource(source: string, cutIndex: nu
|
|
|
143
144
|
type: 'dissolve' | 'fade-black' | 'fade-white' | 'reveal-down' | 'reveal-up';
|
|
144
145
|
duration: number;
|
|
145
146
|
} | null): string;
|
|
147
|
+
export interface SetV2TransitionOutWithHandleInput {
|
|
148
|
+
itemId: string;
|
|
149
|
+
transitionOut: {
|
|
150
|
+
type: 'dissolve' | 'fade-black' | 'fade-white' | 'reveal-down' | 'reveal-up';
|
|
151
|
+
duration: number;
|
|
152
|
+
};
|
|
153
|
+
earlierEndSeconds: number;
|
|
154
|
+
laterStartSeconds: number;
|
|
155
|
+
maxExtendSeconds: number;
|
|
156
|
+
fps: number;
|
|
157
|
+
}
|
|
158
|
+
export interface SetV2TransitionOutWithHandleResult {
|
|
159
|
+
source: string;
|
|
160
|
+
plan: TransitionHandleExtensionPlan;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* v2 の transition_out 宣言と outgoing の source.out / duration 延長を 1 回の
|
|
164
|
+
* byte-preserving 手術で行う。maxExtendSeconds は出力秒軸の上限で、素材実尺は呼び出し側が決める。
|
|
165
|
+
*/
|
|
166
|
+
export declare function setV2TransitionOutWithHandleInSource(source: string, input: SetV2TransitionOutWithHandleInput): SetV2TransitionOutWithHandleResult;
|
|
146
167
|
export declare function reorderCutsInSource(source: string, fromIndex: number, toIndex: number): string;
|
|
147
168
|
export declare function splitCutInSource(source: string, cutIndex: number, atSeconds: number): string;
|
|
148
169
|
export declare function deleteCutInSource(source: string, cutIndex: number): {
|
|
@@ -9,6 +9,7 @@ exports.setCutSpeedInSource = setCutSpeedInSource;
|
|
|
9
9
|
exports.updateCutTransformInSource = updateCutTransformInSource;
|
|
10
10
|
exports.updateCutOpacityInSource = updateCutOpacityInSource;
|
|
11
11
|
exports.setCutTransitionOutInSource = setCutTransitionOutInSource;
|
|
12
|
+
exports.setV2TransitionOutWithHandleInSource = setV2TransitionOutWithHandleInSource;
|
|
12
13
|
exports.reorderCutsInSource = reorderCutsInSource;
|
|
13
14
|
exports.splitCutInSource = splitCutInSource;
|
|
14
15
|
exports.deleteCutInSource = deleteCutInSource;
|
|
@@ -37,6 +38,7 @@ exports.removeOverlayInSource = removeOverlayInSource;
|
|
|
37
38
|
exports.writeTimelineTracksInSource = writeTimelineTracksInSource;
|
|
38
39
|
exports.updateArrayElementByIndex = updateArrayElementByIndex;
|
|
39
40
|
exports.updateOverlayVarInSource = updateOverlayVarInSource;
|
|
41
|
+
const cut_adjacency_1 = require("./cut-adjacency");
|
|
40
42
|
const JSON_NUMBER = '-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?';
|
|
41
43
|
const LAYER_BLEND_MODES = [
|
|
42
44
|
'normal', 'screen', 'multiply', 'add', 'difference',
|
|
@@ -348,6 +350,104 @@ function setCutTransitionOutInSource(source, cutIndex, transitionOut) {
|
|
|
348
350
|
}
|
|
349
351
|
});
|
|
350
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* v2 の transition_out 宣言と outgoing の source.out / duration 延長を 1 回の
|
|
355
|
+
* byte-preserving 手術で行う。maxExtendSeconds は出力秒軸の上限で、素材実尺は呼び出し側が決める。
|
|
356
|
+
*/
|
|
357
|
+
function setV2TransitionOutWithHandleInSource(source, input) {
|
|
358
|
+
const raw = JSON.parse(source);
|
|
359
|
+
if (raw.version !== 2) {
|
|
360
|
+
throw new Error('v2 へ変換してから編集してください。');
|
|
361
|
+
}
|
|
362
|
+
assertTransitionOutValue(input.transitionOut);
|
|
363
|
+
if (!input.itemId) {
|
|
364
|
+
throw new Error('トランジション対象のアイテム id が空です。');
|
|
365
|
+
}
|
|
366
|
+
const tracks = locateArray(source, 'tracks');
|
|
367
|
+
const trackElements = splitTopLevelElements(tracks.inner);
|
|
368
|
+
const matches = [];
|
|
369
|
+
for (const track of trackElements) {
|
|
370
|
+
let items;
|
|
371
|
+
try {
|
|
372
|
+
items = locateArray(track.text, 'items');
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const item = splitTopLevelElements(items.inner)
|
|
378
|
+
.find(candidate => readStringProperty(candidate.text, 'id') === input.itemId);
|
|
379
|
+
if (item)
|
|
380
|
+
matches.push({ track, items, item });
|
|
381
|
+
}
|
|
382
|
+
if (matches.length !== 1) {
|
|
383
|
+
throw new Error(matches.length === 0
|
|
384
|
+
? `アイテム ${input.itemId} が見つかりません`
|
|
385
|
+
: `アイテム ${input.itemId} が複数あります`);
|
|
386
|
+
}
|
|
387
|
+
const match = matches[0];
|
|
388
|
+
const label = `アイテム ${input.itemId}`;
|
|
389
|
+
const durationFrames = readNumberProperty(match.item.text, 'duration', label);
|
|
390
|
+
const mediaSource = locateTopLevelObjectProperty(match.item.text, 'source');
|
|
391
|
+
if (readStringProperty(mediaSource.text, 'kind') !== 'media') {
|
|
392
|
+
throw new Error(`${label} は映像素材ではありません。`);
|
|
393
|
+
}
|
|
394
|
+
const sourceIn = readNumberProperty(mediaSource.text, 'in', label);
|
|
395
|
+
const sourceOut = readNumberProperty(mediaSource.text, 'out', label);
|
|
396
|
+
const outputDurationSeconds = durationFrames / input.fps;
|
|
397
|
+
const speed = (sourceOut - sourceIn) / outputDurationSeconds;
|
|
398
|
+
// 暗黙 speed を維持できない極端値は、素材窓を壊すより宣言だけを残す保守側へ倒す。
|
|
399
|
+
// 実用域外(1e-6 未満 / 1e6 超)の比率も浮動小数の増分が不安定なので延長しない。
|
|
400
|
+
const safeSpeed = Number.isFinite(speed) && speed >= 1e-6 && speed <= 1e6;
|
|
401
|
+
const plan = (0, cut_adjacency_1.planTransitionHandleExtension)({
|
|
402
|
+
declaredSeconds: input.transitionOut.duration,
|
|
403
|
+
earlierEndSeconds: input.earlierEndSeconds,
|
|
404
|
+
laterStartSeconds: input.laterStartSeconds,
|
|
405
|
+
maxExtendSeconds: safeSpeed ? input.maxExtendSeconds : 0,
|
|
406
|
+
fps: input.fps
|
|
407
|
+
});
|
|
408
|
+
let nextMediaSource = writeTransitionOutProperty(mediaSource.text, input.transitionOut);
|
|
409
|
+
if (plan.appliedFrames > 0) {
|
|
410
|
+
const nextOut = sourceOut + plan.appliedSeconds * speed;
|
|
411
|
+
if (!Number.isFinite(nextOut)) {
|
|
412
|
+
throw new Error(`${label} の out を安全に延長できません。`);
|
|
413
|
+
}
|
|
414
|
+
nextMediaSource = replaceNumberProperty(nextMediaSource, 'out', nextOut, label);
|
|
415
|
+
}
|
|
416
|
+
let nextItem = match.item.text.slice(0, mediaSource.start)
|
|
417
|
+
+ nextMediaSource
|
|
418
|
+
+ match.item.text.slice(mediaSource.end);
|
|
419
|
+
if (plan.appliedFrames > 0) {
|
|
420
|
+
nextItem = replaceNumberProperty(nextItem, 'duration', durationFrames + plan.appliedFrames, label);
|
|
421
|
+
}
|
|
422
|
+
const nextTrack = replaceElement(match.track.text, match.items.openIndex + 1, match.item, nextItem);
|
|
423
|
+
return {
|
|
424
|
+
source: replaceElement(source, tracks.openIndex + 1, match.track, nextTrack),
|
|
425
|
+
plan
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function assertTransitionOutValue(transitionOut) {
|
|
429
|
+
if (transitionOut.type !== 'dissolve' && transitionOut.type !== 'fade-black'
|
|
430
|
+
&& transitionOut.type !== 'fade-white' && transitionOut.type !== 'reveal-down'
|
|
431
|
+
&& transitionOut.type !== 'reveal-up') {
|
|
432
|
+
throw new Error('トランジションの種別が不正です。');
|
|
433
|
+
}
|
|
434
|
+
if (!Number.isFinite(transitionOut.duration) || transitionOut.duration <= 0) {
|
|
435
|
+
throw new Error('トランジションの尺は正の数で指定してください。');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function writeTransitionOutProperty(mediaSource, transitionOut) {
|
|
439
|
+
const value = { type: transitionOut.type, duration: transitionOut.duration };
|
|
440
|
+
if (!hasTopLevelProperty(mediaSource, 'transition_out')) {
|
|
441
|
+
return appendJsonProperty(mediaSource, 'transition_out', value);
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
const located = locateTopLevelObjectProperty(mediaSource, 'transition_out');
|
|
445
|
+
return mediaSource.slice(0, located.start) + JSON.stringify(value) + mediaSource.slice(located.end);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
return appendJsonProperty(removeObjectProperty(mediaSource, 'transition_out'), 'transition_out', value);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
351
451
|
function reorderCutsInSource(source, fromIndex, toIndex) {
|
|
352
452
|
const array = locateArray(source, 'cuts');
|
|
353
453
|
const elements = splitTopLevelElements(array.inner);
|
|
@@ -167,6 +167,19 @@ export declare function readInternalSources(source: string | unknown): InternalS
|
|
|
167
167
|
* 検証される側であり、検証対象自身を尺の分母に混ぜると常に「収まっている」判定になってしまう。
|
|
168
168
|
*/
|
|
169
169
|
export declare function visualContentEndSeconds(internal: InternalEdit): number;
|
|
170
|
+
export interface CrossTrackLayerEvacuation {
|
|
171
|
+
itemId: string;
|
|
172
|
+
trackId: string;
|
|
173
|
+
causeItemId: string;
|
|
174
|
+
causeTrackId: string;
|
|
175
|
+
overlapStartFrames: number;
|
|
176
|
+
overlapEndFrames: number;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 別 visual track との重なりが原因で upper item が layers へ退避される組を返す。
|
|
180
|
+
* edit-lint と UI は理由文言に必要な相手 id を、この単一定義から得る。
|
|
181
|
+
*/
|
|
182
|
+
export declare function findCrossTrackLayerEvacuations(edit: unknown): CrossTrackLayerEvacuation[];
|
|
170
183
|
export interface LegacyEditView {
|
|
171
184
|
cuts: EditCut[];
|
|
172
185
|
sources?: EditSource[];
|
|
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
exports.readInternalEdit = readInternalEdit;
|
|
8
8
|
exports.readInternalSources = readInternalSources;
|
|
9
9
|
exports.visualContentEndSeconds = visualContentEndSeconds;
|
|
10
|
+
exports.findCrossTrackLayerEvacuations = findCrossTrackLayerEvacuations;
|
|
10
11
|
exports.projectLegacyEdit = projectLegacyEdit;
|
|
11
12
|
exports.toLegacyTrack = toLegacyTrack;
|
|
12
13
|
exports.derivedLegacyTracks = derivedLegacyTracks;
|
|
@@ -258,26 +259,16 @@ function needsLayersEngine(item, chromaKeyOf, hasOverlappingSibling = false) {
|
|
|
258
259
|
return true;
|
|
259
260
|
return false;
|
|
260
261
|
}
|
|
261
|
-
|
|
262
|
-
// 別トラック間の同時表示も winner-take-all になり、下段を描画できない。全 visual トラックの
|
|
263
|
-
// items[] を総当りで比較し、半開区間 [at, at+duration) が正の長さで交差する media アイテムを
|
|
264
|
-
// layers へ退避する。終端と開始が接するだけなら交差ではない。同一トラックの交差は両方を
|
|
265
|
-
// layers へ送る。別トラックの交差は、下段を cuts の不透明な基底として残し、上段が全画面不透明
|
|
266
|
-
// ではない宣言を持つときだけ layers へ送る。上段が全画面不透明なら winner-take-all でも同じ絵に
|
|
267
|
-
// なるため、従来の cuts / track-stack 経路を保つ。
|
|
268
|
-
//
|
|
269
|
-
// transition_out は同一トラックの隣接カットが意図的に作る狭い重なりで、cuts/xfade が表現する。
|
|
270
|
-
// その例外は同一トラックのペアだけに適用する。別トラックとの交差は transition の有無にかかわらず
|
|
271
|
-
// 真の同時表示なので layers へ退避する。
|
|
272
|
-
function computeOverlappingItemIds(itemGroups) {
|
|
262
|
+
function analyzeOverlappingItems(itemGroups) {
|
|
273
263
|
const overlapping = new Set();
|
|
274
|
-
const
|
|
264
|
+
const crossTrackEvacuations = [];
|
|
265
|
+
const entries = itemGroups.flatMap((group, trackIndex) => group.items.map(item => ({ item, trackIndex, trackId: group.trackId })));
|
|
275
266
|
for (let i = 0; i < entries.length; i++) {
|
|
276
|
-
const { item: a, trackIndex: aTrackIndex } = entries[i];
|
|
267
|
+
const { item: a, trackIndex: aTrackIndex, trackId: aTrackId } = entries[i];
|
|
277
268
|
if (a.source.kind !== 'media')
|
|
278
269
|
continue;
|
|
279
270
|
for (let j = i + 1; j < entries.length; j++) {
|
|
280
|
-
const { item: b, trackIndex: bTrackIndex } = entries[j];
|
|
271
|
+
const { item: b, trackIndex: bTrackIndex, trackId: bTrackId } = entries[j];
|
|
281
272
|
if (b.source.kind !== 'media')
|
|
282
273
|
continue;
|
|
283
274
|
if (!(a.at < b.at + b.duration && b.at < a.at + a.duration))
|
|
@@ -291,13 +282,40 @@ function computeOverlappingItemIds(itemGroups) {
|
|
|
291
282
|
overlapping.add(b.id);
|
|
292
283
|
}
|
|
293
284
|
else {
|
|
294
|
-
const
|
|
295
|
-
|
|
285
|
+
const upperIsA = aTrackIndex > bTrackIndex;
|
|
286
|
+
const upper = upperIsA ? a : b;
|
|
287
|
+
const lower = upperIsA ? b : a;
|
|
288
|
+
if (needsCrossTrackLayers(upper)) {
|
|
296
289
|
overlapping.add(upper.id);
|
|
290
|
+
crossTrackEvacuations.push({
|
|
291
|
+
itemId: upper.id,
|
|
292
|
+
trackId: upperIsA ? aTrackId : bTrackId,
|
|
293
|
+
causeItemId: lower.id,
|
|
294
|
+
causeTrackId: upperIsA ? bTrackId : aTrackId,
|
|
295
|
+
overlapStartFrames: Math.max(a.at, b.at),
|
|
296
|
+
overlapEndFrames: Math.min(a.at + a.duration, b.at + b.duration)
|
|
297
|
+
});
|
|
298
|
+
}
|
|
297
299
|
}
|
|
298
300
|
}
|
|
299
301
|
}
|
|
300
|
-
return overlapping;
|
|
302
|
+
return { itemIds: overlapping, crossTrackEvacuations };
|
|
303
|
+
}
|
|
304
|
+
function computeOverlappingItemIds(itemGroups) {
|
|
305
|
+
return analyzeOverlappingItems(itemGroups.map((items, index) => ({
|
|
306
|
+
items,
|
|
307
|
+
trackId: String(index)
|
|
308
|
+
}))).itemIds;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* 別 visual track との重なりが原因で upper item が layers へ退避される組を返す。
|
|
312
|
+
* edit-lint と UI は理由文言に必要な相手 id を、この単一定義から得る。
|
|
313
|
+
*/
|
|
314
|
+
function findCrossTrackLayerEvacuations(edit) {
|
|
315
|
+
const parsed = (0, edit_v2_1.readEditV2)(edit);
|
|
316
|
+
return analyzeOverlappingItems(parsed.tracks.flatMap(track => track.lane === 'visual' && 'items' in track
|
|
317
|
+
? [{ items: track.items, trackId: track.id }]
|
|
318
|
+
: [])).crossTrackEvacuations;
|
|
301
319
|
}
|
|
302
320
|
// cuts の winner-take-all が下段を隠してよいのは、上段が全画面を不透明に覆う場合だけ。
|
|
303
321
|
// transform の単位元を明示しただけなら従来経路を維持する。crop / 半透明 / keyframes は、
|
|
@@ -31,6 +31,7 @@ const FALLBACK_CLAUDE_GUIDANCE = [
|
|
|
31
31
|
'- 編集スキルは `.claude/skills/` にあり、`/analyze-footage` などの素の名前で使えます。',
|
|
32
32
|
'- Codex や Cursor など他の AI エージェント用の入り口が `.agents/skills/`、`.cursor/skills/`、`.codex/skills/` にあります(中身は `.claude/skills/` へのリンク)。',
|
|
33
33
|
'- `.akari/intake.json` の `status` が `submitted` なら、そこに書かれた `tasks` / `target` / `autonomy` に従って進めます。`autonomy` が `checkpoint`(既定)なら、企画の承認や書き出し前などの要所で必ず人に確認します。`status` が `draft` のときは進め方がまだ決まっていないので、フォームや対話で確定させてから作業を始めます。',
|
|
34
|
+
'- 進め方を `.akari/intake.json` に書くときは、`tasks` は決められた 5 つの id だけを使い、`target` は `duration_s` か `keep_length: true` のどちらか片方にします。`status` を `submitted` にする前に lint で確認します。',
|
|
34
35
|
'- 利用者へは日本語で、内部の仕組みではなく「変更履歴」「企画メモ」「素材」などの言葉で説明します。',
|
|
35
36
|
'',
|
|
36
37
|
'このファイルはあなたのプロジェクトのものです。自由に書き換えて構いません。',
|
|
@@ -52,6 +53,8 @@ const FALLBACK_AGENT_GUIDANCE = [
|
|
|
52
53
|
'`.akari/intake.json` の `status` が `submitted` なら `tasks` / `target` / `autonomy` に従って進める。',
|
|
53
54
|
'`autonomy: checkpoint`(既定)なら企画承認・書き出し前などの要所で人に確認する。',
|
|
54
55
|
'`status: draft` なら進め方が未確定のため、フォームまたは対話で確定させてから進める。',
|
|
56
|
+
'進め方を `.akari/intake.json` に書くときは、`tasks` は決められた 5 つの id だけを使い、`target` は `duration_s` か `keep_length: true` のどちらか片方にする。',
|
|
57
|
+
'`status` を `submitted` にする前に lint で確認する。',
|
|
55
58
|
'',
|
|
56
59
|
'利用者へは日本語で、内部の仕組みではなく役割が伝わる言葉を使う。',
|
|
57
60
|
'この案内はこのプロジェクトのものです。自由に書き換えて構いません。',
|
|
@@ -51,6 +51,14 @@ test("bare template: scaffold generates a draft .akari/intake.json and a CLAUDE.
|
|
|
51
51
|
assert.match(claudeMd, /submitted/);
|
|
52
52
|
assert.match(claudeMd, /draft/);
|
|
53
53
|
assert.match(claudeMd, /checkpoint/);
|
|
54
|
+
assert.match(claudeMd, /`tasks` は決められた 5 つの id だけ/);
|
|
55
|
+
assert.match(claudeMd, /`duration_s` か `keep_length: true` のどちらか片方/);
|
|
56
|
+
assert.match(claudeMd, /`submitted` にする前に lint で確認/);
|
|
57
|
+
|
|
58
|
+
const agentsMd = await readFile(join(destination, "AGENTS.md"), "utf8");
|
|
59
|
+
assert.match(agentsMd, /`tasks` は決められた 5 つの id だけ/);
|
|
60
|
+
assert.match(agentsMd, /`duration_s` か `keep_length: true` のどちらか片方/);
|
|
61
|
+
assert.match(agentsMd, /`submitted` にする前に lint で確認/);
|
|
54
62
|
|
|
55
63
|
assert.ok(report.fallback.writtenFiles.includes(".akari/intake.json"));
|
|
56
64
|
assert.ok(report.fallback.writtenFiles.includes("CLAUDE.md"));
|
|
@@ -64,3 +64,24 @@ akari new <target-dir> [--template <path>]
|
|
|
64
64
|
## intake.json(進め方フォームの保存先)
|
|
65
65
|
|
|
66
66
|
作成直後の `.akari/intake.json` は `status: "draft"`・空 `tasks` から始まる(契約: `packages/schemas/intake.schema.json`)。プロジェクトの CLAUDE.md には intake の規律を追記する — `status: "submitted"` なら `tasks` / `target` / `autonomy` に従い、`autonomy: "checkpoint"`(既定)なら企画承認・書き出し前などの要所で人に確認し、`status: "draft"` のままなら進め方をフォームまたは対話で確定させてから進める。
|
|
67
|
+
|
|
68
|
+
- `tasks` は次の固定語彙のみを使う。日本語ラベルの正本は契約正本の `x-akari-labels` とする
|
|
69
|
+
- `transcribe-captions`(文字起こし・テロップ)
|
|
70
|
+
- `silence-cut`(いらない間・NG のカット)
|
|
71
|
+
- `bgm-sfx`(BGM・効果音)
|
|
72
|
+
- `narration`(ナレーション(自分の声 / 既製の声))
|
|
73
|
+
- `3d-inserts`(3D・画面はめ込みの演出)
|
|
74
|
+
- 依頼文・自由文を `tasks` に入れない。語彙に無い要望は `tasks` に書かず、
|
|
75
|
+
`target.taste`(自由記述)や `planning/` 側へ置く
|
|
76
|
+
- `target.duration_s`(数値指定)と `target.keep_length: true`(尺維持)はどちらか片方にする。
|
|
77
|
+
「約60秒にする」なら `{"duration_s": 60, "keep_length": false}`、「尺は変えない」なら
|
|
78
|
+
`{"duration_s": null, "keep_length": true}` とする
|
|
79
|
+
- `status` を `"submitted"` へ変える前に、リポジトリルートから edit-lint スキルと同じ CLI を
|
|
80
|
+
実行し、`intake.*` の error finding が無いことを確認する。`submitted` にするときは
|
|
81
|
+
`submitted_at` に ISO 8601 日時を必ず入れる
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
node packages/edit-lint/bin/edit-lint.mjs <project-root>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
フィールドの契約正本は `packages/schemas/intake.schema.json` とする。
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
従って進める。`autonomy: checkpoint`(既定)なら企画承認・書き出し前などの要所で
|
|
14
14
|
利用者に確認する。`status: draft` なら進め方が未確定のため、フォームまたは対話で
|
|
15
15
|
確定させてから進める。
|
|
16
|
+
- 進め方を `.akari/intake.json` に書くときは、`tasks` は決められた 5 つの id だけを使い、`target` は `duration_s` か `keep_length: true` のどちらか片方にする。
|
|
17
|
+
`status` を `submitted` にする前に lint で確認する。
|
|
16
18
|
- プロジェクトルート直下に新規ファイルを作らない(`edit.json` 等の既存契約ファイルを除く)。
|
|
17
19
|
生成物は `.akari/work/`、証跡は `.akari/reports/`、キャッシュは `.akari/cache/` に置く。
|
|
18
20
|
詳しい層の定義は `docs/contract-2026-07-25-project-structure-v0.md`(本モノレポ側)を参照する。
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
要所で必ず利用者に確認します。`status` が `draft` のときは進め方がまだ決まっていないので、
|
|
20
20
|
フォームまたは対話で確定させてから作業を始めます。
|
|
21
21
|
|
|
22
|
+
進め方を `.akari/intake.json` に書くときは、`tasks` は決められた 5 つの id だけを使い、`target` は `duration_s` か `keep_length: true` のどちらか片方にします。`status` を `submitted` にする前に lint で確認します。
|
|
23
|
+
|
|
22
24
|
素材が足りないときは、`akari assets list` でアカウントの素材ライブラリ(無料全部 + 購入済み)を
|
|
23
25
|
確認できます。使いたい素材が見つかったら `akari assets fetch <id> --project .` でこのプロジェクトへ
|
|
24
26
|
取り込みます(sha256 検証込み)。有料素材は `akari store connect` で接続済みのアカウントで
|