akari-video 0.1.55 → 0.1.56

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.55",
3
+ "version": "0.1.56",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.55",
3
+ "version": "0.1.56",
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": [
@@ -308,16 +308,19 @@ export async function lintProject(input, options = {}) {
308
308
  paths,
309
309
  structure.sourceIds,
310
310
  );
311
- // 総尺は edit-store の共通定義で決める。映像本体がある間は従来の visual 最大終端のままで、
312
- // 0 秒のときだけ overlays / 音声の終端へ後退する。cuts が構造的に不正なときは従来どおり
313
- // timeline を null にして下流の尺検証を止める。
311
+ // 全素材の最大終端を出力尺とする。外部字幕の output 区間も同じ出力軸へ加える。
312
+ // cuts が構造的に不正なら下流の尺検証は止める。
314
313
  const timelineDuration = cutsStructureResult === null ? null : timelineDurationSeconds(internalEdit);
315
- const timeline = timelineDuration?.seconds ?? null;
314
+ const captionRows = Array.isArray(captionsState.value) ? captionsState.value
315
+ : Array.isArray(captionsState.value?.captions) ? captionsState.value.captions : [];
316
+ const outputCaptionEnd = captionRows.reduce((end, cue) =>
317
+ cue?.time_domain === 'output' && Number.isFinite(cue.end) ? Math.max(end, cue.end) : end, 0);
318
+ const timeline = timelineDuration ? Math.max(timelineDuration.seconds, outputCaptionEnd) : null;
316
319
  if (timelineDuration?.basis === "overlays-audio") {
317
320
  addFinding(findings, {
318
321
  severity: "info",
319
322
  check: "timeline.duration-derived",
320
- message: `映像素材が無いため尺を overlays / 音声の終端 ${formatNumber(timeline)} 秒から導出した`,
323
+ message: `尺を overlays / 字幕 / 音声の終端 ${formatNumber(timeline)} 秒から導出した`,
321
324
  path: "edit.json#tracks",
322
325
  });
323
326
  }
@@ -326,16 +329,10 @@ export async function lintProject(input, options = {}) {
326
329
  validateStillImageCuts(edit, findings);
327
330
  const cutTrackSegments = computeCutTrackSegments(edit.cuts);
328
331
  validateTransitionAdjacency(edit.cuts, cutTrackSegments, edit.sources, edit.fps, findings);
329
- for (const segment of findTrackOverlaps(cutTrackSegments)) {
330
- if (isDeclaredTransitionOverlap(edit.cuts, cutTrackSegments, segment, edit.fps)) continue;
331
- addFinding(findings, {
332
- severity: "error",
333
- check: "cuts.track-overlap",
334
- message: `cut overlaps another cut on track ${segment.track} in the output axis`,
335
- path: `edit.json#cuts[${segment.index}]`,
336
- range: { start: segment.start, end: segment.end },
337
- });
338
- }
332
+ // v2.track-no-overlap above checks the actual track IDs before legacy projection.
333
+ // A mixed telop/media track may project its media into cuts with the same numeric
334
+ // ref as another physical track. Those per-kind aliases are not v2 track identity;
335
+ // checking projected cuts again would reject valid cross-track composition.
339
336
  validateDurationMaximum(edit.outputs, timeline, findings, paths);
340
337
  validateOutputAxisDurationMax(edit.outputs, cutTrackSegments, findings);
341
338
  await validateOverlays(edit.overlays, timeline, findings, paths);
@@ -175,6 +175,9 @@ function scheduleBgm(spec, timelineDurationSec, startAtSec, duckIntervals, warni
175
175
  const timelineT = typeof spec.t === 'number' && Number.isFinite(spec.t) && spec.t > 0 ? spec.t : 0;
176
176
  if (timelineT >= timelineDurationSec)
177
177
  return null;
178
+ const itemEndSec = finitePositive(spec.duration) ? Math.min(timelineDurationSec, timelineT + spec.duration) : timelineDurationSec;
179
+ if (startAtSec >= itemEndSec)
180
+ return null;
178
181
  const sidecar = validSidecar(spec.sidecar);
179
182
  if (spec.sidecar && !sidecar)
180
183
  warnings.push(`${label}: sidecar declaration is invalid; using source`);
@@ -196,7 +199,7 @@ function scheduleBgm(spec, timelineDurationSec, startAtSec, duckIntervals, warni
196
199
  return null;
197
200
  }
198
201
  const timelineStartSec = startAtSec + delaySec;
199
- const timelineAvailableSec = timelineDurationSec - timelineStartSec;
202
+ const timelineAvailableSec = itemEndSec - timelineStartSec;
200
203
  const durationSec = Math.min(timelineAvailableSec, loop ? timelineAvailableSec : (materialDurationSec - sourceOffsetSec) / playbackRate);
201
204
  if (!(durationSec > 0))
202
205
  return null;
@@ -214,8 +217,8 @@ function scheduleBgm(spec, timelineDurationSec, startAtSec, duckIntervals, warni
214
217
  sourceDurationSec: durationSec * playbackRate,
215
218
  loop,
216
219
  gainDb,
217
- gainEvents: bgmFadeGainEvents(spec.fadeIn, spec.fadeOut, timelineDurationSec, timelineStartSec, durationSec, baseGain),
218
- envelopeEvents: scheduledEnvelopeEvents(spec, timelineT, timelineDurationSec - timelineT, elapsedSec, durationSec, duckIntervals)
220
+ gainEvents: bgmFadeGainEvents(spec.fadeIn, spec.fadeOut, itemEndSec, timelineStartSec, durationSec, baseGain),
221
+ envelopeEvents: scheduledEnvelopeEvents(spec, timelineT, itemEndSec - timelineT, elapsedSec, durationSec, duckIntervals)
219
222
  };
220
223
  }
221
224
  function scheduleSpeech(spec, timelineDurationSec, startAtSec, warnings) {
@@ -42,6 +42,8 @@ export interface EditOverlay {
42
42
  }
43
43
  export type LayerBlendMode = 'normal' | 'screen' | 'multiply' | 'add' | 'difference' | 'darken' | 'lighten' | 'overlay' | 'hardlight' | 'softlight';
44
44
  export interface EditLayer {
45
+ in?: number;
46
+ speed?: number;
45
47
  id: string;
46
48
  t: number;
47
49
  duration: number;
@@ -132,6 +134,8 @@ export interface CutTrackSegment {
132
134
  end: number;
133
135
  }
134
136
  export interface EditAudioBgm {
137
+ t?: number;
138
+ duration?: number;
135
139
  id: 'bgm';
136
140
  path: string;
137
141
  track?: number;
@@ -202,9 +202,8 @@ export declare function readInternalSources(source: string | unknown): InternalS
202
202
  */
203
203
  export declare function visualContentEndSeconds(internal: InternalEdit): number;
204
204
  /**
205
- * 出力タイムラインの総尺。映像本体がある間は visualContentEndSeconds を唯一の正本とし、
206
- * 映像本体が 0 秒のときだけ overlays / 字幕 / narration / sfx の最大終端へ後退する。
207
- * BGM は総尺に合わせて切られる素材なので、後退尺には含めない。
205
+ * 出力タイムラインの総尺は全素材の最大終端。映像の後ろの HTML・字幕・音声も出力対象。
206
+ * 実尺未解決(duration=0)の音源はここでは延長せず、再生・書き出し時のプローブで補完する。
208
207
  */
209
208
  export declare function timelineDurationSeconds(internal: InternalEdit): {
210
209
  seconds: number;
@@ -77,21 +77,17 @@ function visualContentEndSeconds(internal) {
77
77
  return maxEnd;
78
78
  }
79
79
  /**
80
- * 出力タイムラインの総尺。映像本体がある間は visualContentEndSeconds を唯一の正本とし、
81
- * 映像本体が 0 秒のときだけ overlays / 字幕 / narration / sfx の最大終端へ後退する。
82
- * BGM は総尺に合わせて切られる素材なので、後退尺には含めない。
80
+ * 出力タイムラインの総尺は全素材の最大終端。映像の後ろの HTML・字幕・音声も出力対象。
81
+ * 実尺未解決(duration=0)の音源はここでは延長せず、再生・書き出し時のプローブで補完する。
83
82
  */
84
83
  function timelineDurationSeconds(internal) {
85
84
  const visualEnd = visualContentEndSeconds(internal);
86
- if (visualEnd > 0) {
87
- return { seconds: visualEnd, basis: 'visual' };
88
- }
89
85
  let fallbackEnd = 0;
90
86
  const walk = (item, lane) => {
91
87
  const isFallbackVisual = lane === 'visual'
92
88
  && ['html', 'group', 'captions', 'caption'].includes(item.source.kind);
93
89
  const isFallbackAudio = lane === 'audio'
94
- && (item.legacy.collection === 'narration' || item.legacy.collection === 'sfx');
90
+ && item.duration > 0;
95
91
  if (isFallbackVisual || isFallbackAudio) {
96
92
  fallbackEnd = Math.max(fallbackEnd, item.at + item.duration);
97
93
  }
@@ -102,6 +98,8 @@ function timelineDurationSeconds(internal) {
102
98
  for (const item of track.items)
103
99
  walk(item, track.lane);
104
100
  }
101
+ if (visualEnd >= fallbackEnd && visualEnd > 0)
102
+ return { seconds: visualEnd, basis: 'visual' };
105
103
  return fallbackEnd > 0
106
104
  ? { seconds: fallbackEnd, basis: 'overlays-audio' }
107
105
  : { seconds: 0, basis: 'empty' };
@@ -655,6 +653,7 @@ function buildV2VisualItem(item, fps, ref, pathOf, chromaKeyOf, legacyIndexCount
655
653
  if (needsLayersEngine(item, chromaKeyOf, hasOverlappingSibling)) {
656
654
  const declaration = {
657
655
  id: item.id, t: at, duration, kind: 'video', src: path ?? item.source.src,
656
+ in: item.source.in,
658
657
  track: ref, ...common, ...copyMediaSourceFields(item.source),
659
658
  ...('audio' in item && item.audio === false ? { audio: false } : {})
660
659
  };
@@ -892,6 +891,7 @@ function buildV2AudioItem(item, fps, ref, pathOf, legacyIndexCounters) {
892
891
  if (role === 'bgm') {
893
892
  const value = {
894
893
  id: 'bgm',
894
+ ...(duration > 0 ? { t: at, duration } : {}),
895
895
  path: resolvedPath,
896
896
  track: ref,
897
897
  ...(item.fade_in !== undefined ? { fadeIn: item.fade_in } : {}),
@@ -910,6 +910,7 @@ function buildV2AudioItem(item, fps, ref, pathOf, legacyIndexCounters) {
910
910
  id: item.id, atFrames, durationFrames, at, duration, children: [], source,
911
911
  declaration: {
912
912
  path: resolvedPath,
913
+ ...(duration > 0 ? { t: at, duration } : {}),
913
914
  ...(item.source.in !== undefined ? { in: item.source.in } : {}),
914
915
  ...(item.fade_in !== undefined ? { fadeIn: item.fade_in } : {}),
915
916
  ...(item.fade_out !== undefined ? { fadeOut: item.fade_out } : {}),
@@ -43,7 +43,7 @@ const CUT_KEYS = new Set([
43
43
  ]);
44
44
  const OVERLAY_KEYS = new Set(['id', 'html', 'start', 'duration', 'vars', 'transform', 'track']);
45
45
  const LAYER_KEYS = new Set([
46
- 'id', 't', 'duration', 'kind', 'src', 'transform', 'crop', 'perspective', 'opacity',
46
+ 'id', 't', 'duration', 'kind', 'src', 'in', 'speed', 'transform', 'crop', 'perspective', 'opacity',
47
47
  'keyframes', 'preset', 'params', 'track', 'blend', 'chroma_key', 'filter', 'mask'
48
48
  ]);
49
49
  const AUDIO_ENVELOPE_KEYS = ['keyframes', 'ducking', 'duck_db', 'duck_attack', 'duck_release'];
@@ -217,7 +217,7 @@ function migrateEditToV2(raw, options = {}) {
217
217
  let source;
218
218
  let mask;
219
219
  if (value.kind === 'filter') {
220
- const forbidden = ['src', 'chroma_key', 'blend', 'crop', 'transform', 'mask'].filter(key => hasOwn(value, key));
220
+ const forbidden = ['src', 'in', 'speed', 'chroma_key', 'blend', 'crop', 'transform', 'mask'].filter(key => hasOwn(value, key));
221
221
  if (forbidden.length > 0) {
222
222
  blockers.push(`edit.json.layers[${index}] の kind filter は ${forbidden.join(' / ')} を持てません。`);
223
223
  return;
@@ -229,6 +229,10 @@ function migrateEditToV2(raw, options = {}) {
229
229
  return;
230
230
  }
231
231
  else if (value.kind === 'baked' && nonEmpty(value.preset)) {
232
+ if (hasOwn(value, 'in') || hasOwn(value, 'speed')) {
233
+ blockers.push(`edit.json.layers[${index}] の in / speed は baked telop へ変換できません。`);
234
+ return;
235
+ }
232
236
  if (hasOwn(value, 'mask')) {
233
237
  blockers.push(`edit.json.layers[${index}].mask は baked telop へ変換できません。`);
234
238
  return;
@@ -239,6 +243,13 @@ function migrateEditToV2(raw, options = {}) {
239
243
  };
240
244
  }
241
245
  else {
246
+ if ((hasOwn(value, 'in') && !nonNegative(value.in))
247
+ || (hasOwn(value, 'speed') && !positive(value.speed))) {
248
+ blockers.push(`edit.json.layers[${index}] の in / speed が不正です。`);
249
+ return;
250
+ }
251
+ const sourceIn = hasOwn(value, 'in') ? value.in : 0;
252
+ const speed = hasOwn(value, 'speed') ? value.speed : 1;
242
253
  let src = sourceIdByPath.get(value.src);
243
254
  if (!src) {
244
255
  do
@@ -248,7 +259,10 @@ function migrateEditToV2(raw, options = {}) {
248
259
  sourceIdByPath.set(value.src, src);
249
260
  sources.push({ id: src, path: value.src, proxy: null });
250
261
  }
251
- source = { kind: 'media', src, in: 0, out: value.duration, ...copyPresent(value, ['chroma_key']) };
262
+ source = {
263
+ kind: 'media', src, in: sourceIn, out: sourceIn + value.duration * speed,
264
+ ...copyPresent(value, ['speed', 'chroma_key'])
265
+ };
252
266
  if (hasOwn(value, 'mask')) {
253
267
  if (!nonEmpty(value.mask)) {
254
268
  blockers.push(`edit.json.layers[${index}].mask が不正です。`);
@@ -1319,6 +1319,8 @@ var AkariEditKernel = (() => {
1319
1319
  if (gainDb === null) return null;
1320
1320
  const timelineT = typeof spec.t === "number" && Number.isFinite(spec.t) && spec.t > 0 ? spec.t : 0;
1321
1321
  if (timelineT >= timelineDurationSec) return null;
1322
+ const itemEndSec = finitePositive(spec.duration) ? Math.min(timelineDurationSec, timelineT + spec.duration) : timelineDurationSec;
1323
+ if (startAtSec >= itemEndSec) return null;
1322
1324
  const sidecar = validSidecar(spec.sidecar);
1323
1325
  if (spec.sidecar && !sidecar) warnings.push(`${label}: sidecar declaration is invalid; using source`);
1324
1326
  const materialDurationSec = sidecar ? sidecar.durationSec : spec.durationSec;
@@ -1338,7 +1340,7 @@ var AkariEditKernel = (() => {
1338
1340
  return null;
1339
1341
  }
1340
1342
  const timelineStartSec = startAtSec + delaySec;
1341
- const timelineAvailableSec = timelineDurationSec - timelineStartSec;
1343
+ const timelineAvailableSec = itemEndSec - timelineStartSec;
1342
1344
  const durationSec = Math.min(
1343
1345
  timelineAvailableSec,
1344
1346
  loop ? timelineAvailableSec : (materialDurationSec - sourceOffsetSec) / playbackRate
@@ -1361,7 +1363,7 @@ var AkariEditKernel = (() => {
1361
1363
  gainEvents: bgmFadeGainEvents(
1362
1364
  spec.fadeIn,
1363
1365
  spec.fadeOut,
1364
- timelineDurationSec,
1366
+ itemEndSec,
1365
1367
  timelineStartSec,
1366
1368
  durationSec,
1367
1369
  baseGain
@@ -1369,7 +1371,7 @@ var AkariEditKernel = (() => {
1369
1371
  envelopeEvents: scheduledEnvelopeEvents(
1370
1372
  spec,
1371
1373
  timelineT,
1372
- timelineDurationSec - timelineT,
1374
+ itemEndSec - timelineT,
1373
1375
  elapsedSec,
1374
1376
  durationSec,
1375
1377
  duckIntervals
@@ -2911,6 +2913,7 @@ var AkariEditKernel = (() => {
2911
2913
  duration,
2912
2914
  kind: "video",
2913
2915
  src: path ?? item.source.src,
2916
+ in: item.source.in,
2914
2917
  track: ref,
2915
2918
  ...common,
2916
2919
  ...copyMediaSourceFields(item.source),
@@ -3233,6 +3236,7 @@ var AkariEditKernel = (() => {
3233
3236
  if (role === "bgm") {
3234
3237
  const value2 = {
3235
3238
  id: "bgm",
3239
+ ...duration > 0 ? { t: at, duration } : {},
3236
3240
  path: resolvedPath,
3237
3241
  track: ref,
3238
3242
  ...item.fade_in !== void 0 ? { fadeIn: item.fade_in } : {},
@@ -3257,6 +3261,7 @@ var AkariEditKernel = (() => {
3257
3261
  source,
3258
3262
  declaration: {
3259
3263
  path: resolvedPath,
3264
+ ...duration > 0 ? { t: at, duration } : {},
3260
3265
  ...item.source.in !== void 0 ? { in: item.source.in } : {},
3261
3266
  ...item.fade_in !== void 0 ? { fadeIn: item.fade_in } : {},
3262
3267
  ...item.fade_out !== void 0 ? { fadeOut: item.fade_out } : {},
@@ -81,7 +81,7 @@
81
81
  { "path": "tracks[].items[].source.freeze", "applies_to": ["layers"], "gpu": "ignored", "osr": "ignored", "runtime_warning": true, "evidence": "packages/edit-store/src/internal-model.ts copyMediaSourceFields; packages/frame-engine/src/timeline/plan.ts KNOWN_LAYER_KEYS omits freeze" },
82
82
  { "path": "tracks[].items[].source.fx", "applies_to": ["cuts", "layers"], "gpu": "ignored", "osr": "ignored", "runtime_warning": true, "evidence": "packages/edit-store/src/internal-model.ts copyMediaSourceFields; packages/frame-engine/src/timeline/plan.ts KNOWN_CUT_KEYS/KNOWN_LAYER_KEYS omit fx", "hint": "GPU / OSR frame-engine には登録済み cut FX が無い" },
83
83
  { "path": "tracks[].items[].source.speed", "applies_to": ["cuts"], "gpu": "consumed", "osr": "consumed", "evidence": "packages/edit-store/src/internal-model.ts buildV2VisualItem speed projection; packages/frame-engine/src/timeline/plan.ts buildResolvedTimelinePlan/layerFromPlacement" },
84
- { "path": "tracks[].items[].source.speed", "applies_to": ["layers"], "gpu": "ignored", "osr": "ignored", "runtime_warning": true, "evidence": "packages/edit-store/src/internal-model.ts copyMediaSourceFields; packages/frame-engine/src/timeline/plan.ts KNOWN_LAYER_KEYS omits speed" },
84
+ { "path": "tracks[].items[].source.speed", "applies_to": ["layers"], "gpu": "consumed", "osr": "consumed", "evidence": "packages/edit-store/src/internal-model.ts copyMediaSourceFields; packages/frame-engine/src/timeline/plan.ts resolvedCompositeLayers preserves source trim and speed" },
85
85
  { "path": "tracks[].items[].source.gain_db", "applies_to": ["cuts"], "gpu": "other-subsystem", "osr": "other-subsystem", "evidence": "packages/render-cut/src/plan.mjs cutSpeechVolumeSuffix (appendSequentialAudioCutFilters / gap-aware); packages/edit-store/src/audio-schedule.ts projectSpeechDeclarations speechGainDb", "note": "埋め込み音声(speech)の減衰。映像エンジンではなく render-cut の cut 音声(全エンジン共通)とプレビューの音声供給が消費する" },
86
86
  { "path": "tracks[].items[].source.mute", "applies_to": ["cuts"], "gpu": "other-subsystem", "osr": "other-subsystem", "evidence": "packages/render-cut/src/plan.mjs cut.mute -> anullsrc branch; packages/edit-store/src/audio-schedule.ts projectSpeechDeclarations skips muted cuts", "note": "埋め込み音声(speech)のミュート。尺は保ち無音区間になる" },
87
87
  { "path": "tracks[].items[].source.chroma_key", "applies_to": ["cuts", "layers"], "gpu": "partial", "osr": "partial", "runtime_warning": true, "evidence": "packages/edit-store/src/internal-model.ts needsLayersEngine/copyMediaSourceFields; packages/frame-engine/src/timeline/plan.ts KNOWN_CUT_KEYS/KNOWN_LAYER_KEYS omit chroma_key", "note": "透過 intent は layers 振り分けに使うが、frame-engine 自体は chroma key 抽出や background 置換をしない" },