akari-video 0.1.59 → 0.1.61

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.59",
3
+ "version": "0.1.61",
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.59",
3
+ "version": "0.1.61",
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": [
@@ -1,5 +1,5 @@
1
1
  import { DuckInterval } from './ducking';
2
- import type { EditCut } from './edit-store';
2
+ import { type EditCut } from './edit-store';
3
3
  export type WebAudioScheduleKind = 'bgm' | 'sfx' | 'narration' | 'speech';
4
4
  export interface WebAudioDecodedItem {
5
5
  id?: string;
@@ -6,6 +6,7 @@ const audio_ownership_1 = require("./audio-ownership");
6
6
  const ducking_1 = require("./ducking");
7
7
  const envelope_1 = require("./envelope");
8
8
  const timeline_map_1 = require("./timeline-map");
9
+ const edit_store_1 = require("./edit-store");
9
10
  /**
10
11
  * 解決済みタイムライン尺・正規化済み audio 宣言・デコード実尺を、Web Audio がそのまま
11
12
  * 消費できる予定表へ落とす。fetch/decode/時計は扱わないため、実時間と OfflineAudioContext
@@ -305,21 +306,45 @@ function projectSpeechDeclarations(cuts, options) {
305
306
  return { ...cut, out: cut.out + holdSec * speed };
306
307
  });
307
308
  const map = (0, timeline_map_1.buildTimelineMap)(virtualCuts, { fps });
309
+ // Keep every clip's window, including clips absent from the visual winner map.
310
+ // Match buildTimelineMap's usable-cut filtering before resolving track cursors.
311
+ const usable = virtualCuts.map((cut, index) => ({ cut, index })).filter(({ cut }) => Number.isFinite(cut.in) && Number.isFinite(cut.out) && cut.in < cut.out);
312
+ const clipWindows = new Map((0, edit_store_1.computeCutTrackSegments)(usable.map(entry => entry.cut)).map(segment => [
313
+ usable[segment.index].index,
314
+ { start: segment.at, end: segment.end, cutTimelineStart: segment.at }
315
+ ]));
316
+ for (const window of map.transitionWindows) {
317
+ for (const participant of [window.outgoing, window.incoming]) {
318
+ const clip = clipWindows.get(participant.cutIndex);
319
+ const cut = normalizedCuts[participant.cutIndex];
320
+ if (!clip || !cut || typeof participant.in !== 'number')
321
+ continue;
322
+ const speed = finitePositive(cut.speed) ? cut.speed : 1;
323
+ // Transition slices retain the source mapping after hidden-handle adjustment,
324
+ // even when another track completely covers both participants.
325
+ clip.cutTimelineStart = participant.outStart - (participant.in - cut.in) / speed;
326
+ }
327
+ const outgoing = clipWindows.get(window.outgoing.cutIndex);
328
+ const incoming = clipWindows.get(window.incoming.cutIndex);
329
+ if (outgoing)
330
+ outgoing.end = Math.max(outgoing.end, window.end);
331
+ // scheduleSpeech adds this incoming overlap back via crossfadeInSec. Keep the
332
+ // existing declaration boundary so the transition is not applied twice.
333
+ if (incoming)
334
+ incoming.start = Math.max(incoming.start, window.end);
335
+ }
308
336
  const declarations = [];
309
- for (const segment of map.segments) {
310
- if (segment.kind !== 'src' || segment.cutIndex === null)
311
- continue;
312
- const cut = normalizedCuts[segment.cutIndex];
337
+ for (const [cutIndex, clip] of clipWindows) {
338
+ const cut = normalizedCuts[cutIndex];
313
339
  if (!cut || typeof cut.src !== 'string' || !cut.src)
314
340
  continue;
315
341
  if (!(0, audio_ownership_1.isCutAudioAudible)(cut))
316
342
  continue;
317
343
  const speed = finitePositive(cut.speed) ? cut.speed : 1;
318
- const segmentIn = typeof segment.in === 'number' ? segment.in : cut.in;
319
- const cutTimelineStart = segment.outStart - (segmentIn - cut.in) / speed;
344
+ const cutTimelineStart = clip.cutTimelineStart;
320
345
  const baseDurationSec = Math.max(0, cut.out - cut.in) / speed;
321
346
  const gainDb = speechGainDb(cut);
322
- const baseId = typeof cut.id === 'string' && cut.id ? cut.id : `cut-${segment.cutIndex}`;
347
+ const baseId = speechBaseId(cut, cutIndex);
323
348
  const holdSec = freezeDuration(cut.freeze);
324
349
  if (!(holdSec > 0)) {
325
350
  appendSpeechIntersection(declarations, {
@@ -327,8 +352,8 @@ function projectSpeechDeclarations(cuts, options) {
327
352
  sourceIn: cut.in,
328
353
  outputStart: cutTimelineStart,
329
354
  outputEnd: cutTimelineStart + baseDurationSec,
330
- segmentStart: segment.outStart,
331
- segmentEnd: segment.outEnd,
355
+ clipStart: clip.start,
356
+ clipEnd: clip.end,
332
357
  track: cut.track
333
358
  });
334
359
  continue;
@@ -340,8 +365,8 @@ function projectSpeechDeclarations(cuts, options) {
340
365
  sourceIn: cut.in,
341
366
  outputStart: cutTimelineStart,
342
367
  outputEnd: cutTimelineStart + freezeAtSec,
343
- segmentStart: segment.outStart,
344
- segmentEnd: segment.outEnd,
368
+ clipStart: clip.start,
369
+ clipEnd: clip.end,
345
370
  track: cut.track
346
371
  });
347
372
  appendSpeechIntersection(declarations, {
@@ -349,8 +374,8 @@ function projectSpeechDeclarations(cuts, options) {
349
374
  sourceIn: freezeSourceIn,
350
375
  outputStart: cutTimelineStart + freezeAtSec + holdSec,
351
376
  outputEnd: cutTimelineStart + baseDurationSec + holdSec,
352
- segmentStart: segment.outStart,
353
- segmentEnd: segment.outEnd,
377
+ clipStart: clip.start,
378
+ clipEnd: clip.end,
354
379
  track: cut.track
355
380
  });
356
381
  }
@@ -381,8 +406,8 @@ function speechBaseId(cut, index) {
381
406
  return cut && typeof cut.id === 'string' && cut.id ? cut.id : `cut-${index}`;
382
407
  }
383
408
  function appendSpeechIntersection(declarations, input) {
384
- const atSec = Math.max(input.outputStart, input.segmentStart);
385
- const endSec = Math.min(input.outputEnd, input.segmentEnd);
409
+ const atSec = Math.max(input.outputStart, input.clipStart);
410
+ const endSec = Math.min(input.outputEnd, input.clipEnd);
386
411
  if (!(endSec > atSec))
387
412
  return;
388
413
  const inSec = input.sourceIn + (atSec - input.outputStart) * input.speed;
@@ -1452,18 +1452,34 @@ var AkariEditKernel = (() => {
1452
1452
  return { ...cut, out: cut.out + holdSec * speed };
1453
1453
  });
1454
1454
  const map = buildTimelineMap(virtualCuts, { fps });
1455
+ const usable = virtualCuts.map((cut, index) => ({ cut, index })).filter(({ cut }) => Number.isFinite(cut.in) && Number.isFinite(cut.out) && cut.in < cut.out);
1456
+ const clipWindows = new Map(computeCutTrackSegments(usable.map((entry) => entry.cut)).map((segment) => [
1457
+ usable[segment.index].index,
1458
+ { start: segment.at, end: segment.end, cutTimelineStart: segment.at }
1459
+ ]));
1460
+ for (const window of map.transitionWindows) {
1461
+ for (const participant of [window.outgoing, window.incoming]) {
1462
+ const clip = clipWindows.get(participant.cutIndex);
1463
+ const cut = normalizedCuts[participant.cutIndex];
1464
+ if (!clip || !cut || typeof participant.in !== "number") continue;
1465
+ const speed = finitePositive(cut.speed) ? cut.speed : 1;
1466
+ clip.cutTimelineStart = participant.outStart - (participant.in - cut.in) / speed;
1467
+ }
1468
+ const outgoing = clipWindows.get(window.outgoing.cutIndex);
1469
+ const incoming = clipWindows.get(window.incoming.cutIndex);
1470
+ if (outgoing) outgoing.end = Math.max(outgoing.end, window.end);
1471
+ if (incoming) incoming.start = Math.max(incoming.start, window.end);
1472
+ }
1455
1473
  const declarations = [];
1456
- for (const segment of map.segments) {
1457
- if (segment.kind !== "src" || segment.cutIndex === null) continue;
1458
- const cut = normalizedCuts[segment.cutIndex];
1474
+ for (const [cutIndex, clip] of clipWindows) {
1475
+ const cut = normalizedCuts[cutIndex];
1459
1476
  if (!cut || typeof cut.src !== "string" || !cut.src) continue;
1460
1477
  if (!isCutAudioAudible(cut)) continue;
1461
1478
  const speed = finitePositive(cut.speed) ? cut.speed : 1;
1462
- const segmentIn = typeof segment.in === "number" ? segment.in : cut.in;
1463
- const cutTimelineStart = segment.outStart - (segmentIn - cut.in) / speed;
1479
+ const cutTimelineStart = clip.cutTimelineStart;
1464
1480
  const baseDurationSec = Math.max(0, cut.out - cut.in) / speed;
1465
1481
  const gainDb = speechGainDb(cut);
1466
- const baseId = typeof cut.id === "string" && cut.id ? cut.id : `cut-${segment.cutIndex}`;
1482
+ const baseId = speechBaseId(cut, cutIndex);
1467
1483
  const holdSec = freezeDuration(cut.freeze);
1468
1484
  if (!(holdSec > 0)) {
1469
1485
  appendSpeechIntersection(declarations, {
@@ -1474,8 +1490,8 @@ var AkariEditKernel = (() => {
1474
1490
  sourceIn: cut.in,
1475
1491
  outputStart: cutTimelineStart,
1476
1492
  outputEnd: cutTimelineStart + baseDurationSec,
1477
- segmentStart: segment.outStart,
1478
- segmentEnd: segment.outEnd,
1493
+ clipStart: clip.start,
1494
+ clipEnd: clip.end,
1479
1495
  track: cut.track
1480
1496
  });
1481
1497
  continue;
@@ -1490,8 +1506,8 @@ var AkariEditKernel = (() => {
1490
1506
  sourceIn: cut.in,
1491
1507
  outputStart: cutTimelineStart,
1492
1508
  outputEnd: cutTimelineStart + freezeAtSec,
1493
- segmentStart: segment.outStart,
1494
- segmentEnd: segment.outEnd,
1509
+ clipStart: clip.start,
1510
+ clipEnd: clip.end,
1495
1511
  track: cut.track
1496
1512
  });
1497
1513
  appendSpeechIntersection(declarations, {
@@ -1502,8 +1518,8 @@ var AkariEditKernel = (() => {
1502
1518
  sourceIn: freezeSourceIn,
1503
1519
  outputStart: cutTimelineStart + freezeAtSec + holdSec,
1504
1520
  outputEnd: cutTimelineStart + baseDurationSec + holdSec,
1505
- segmentStart: segment.outStart,
1506
- segmentEnd: segment.outEnd,
1521
+ clipStart: clip.start,
1522
+ clipEnd: clip.end,
1507
1523
  track: cut.track
1508
1524
  });
1509
1525
  }
@@ -1530,8 +1546,8 @@ var AkariEditKernel = (() => {
1530
1546
  return cut && typeof cut.id === "string" && cut.id ? cut.id : `cut-${index}`;
1531
1547
  }
1532
1548
  function appendSpeechIntersection(declarations, input) {
1533
- const atSec = Math.max(input.outputStart, input.segmentStart);
1534
- const endSec = Math.min(input.outputEnd, input.segmentEnd);
1549
+ const atSec = Math.max(input.outputStart, input.clipStart);
1550
+ const endSec = Math.min(input.outputEnd, input.clipEnd);
1535
1551
  if (!(endSec > atSec)) return;
1536
1552
  const inSec = input.sourceIn + (atSec - input.outputStart) * input.speed;
1537
1553
  const outSec = inSec + (endSec - atSec) * input.speed;