akari-video 0.1.21 → 0.1.23

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.
Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/src/migrate-command.mjs +11 -2
  3. package/vendor/.akari-capability-sources.json +3 -0
  4. package/vendor/docs/contract-2026-07-25-project-structure-v0.md +17 -0
  5. package/vendor/docs/contract-2026-08-02-preview-parity.md +39 -0
  6. package/vendor/packages/akari-launcher/package.json +1 -1
  7. package/vendor/packages/analysis-report/package.json +4 -8
  8. package/vendor/packages/analysis-report/render-analysis-report.mjs +544 -0
  9. package/vendor/packages/analysis-report/template.html +2023 -0
  10. package/vendor/packages/edit-lint/src/edit-lint.mjs +20 -0
  11. package/vendor/packages/edit-store/lib/caption-display.d.ts +10 -0
  12. package/vendor/packages/edit-store/lib/caption-display.js +57 -0
  13. package/vendor/packages/edit-store/lib/index.d.ts +1 -0
  14. package/vendor/packages/edit-store/lib/index.js +1 -0
  15. package/vendor/packages/edit-store/lib/internal-model.js +20 -8
  16. package/vendor/packages/edit-store/lib/migrate/index.d.ts +2 -0
  17. package/vendor/packages/edit-store/lib/migrate/index.js +32 -15
  18. package/vendor/packages/edit-store/lib/transition-visual.d.ts +23 -0
  19. package/vendor/packages/edit-store/lib/transition-visual.js +141 -0
  20. package/vendor/packages/edit-store/lib/webview-kernel.d.ts +2 -0
  21. package/vendor/packages/edit-store/lib/webview-kernel.js +139 -0
  22. package/vendor/packages/frame-engine/README.ja.md +24 -0
  23. package/vendor/packages/frame-engine/README.md +24 -0
  24. package/vendor/packages/frame-engine/package.json +27 -0
  25. package/vendor/packages/preview-engine/README.md +38 -0
  26. package/vendor/packages/preview-server/package.json +1 -1
@@ -31,6 +31,9 @@ const {
31
31
  visualContentEndSeconds,
32
32
  TRANSITION_TYPE_IDS,
33
33
  } = createRequire(import.meta.url)("../../edit-store/lib/index.js");
34
+ const { captionsHaveRenderableCues } = createRequire(import.meta.url)(
35
+ "../../edit-store/lib/migrate/index.js",
36
+ );
34
37
 
35
38
  const VERSION = 1;
36
39
  const EPSILON = 1e-6;
@@ -198,6 +201,7 @@ export async function lintProject(input, options = {}) {
198
201
  } else {
199
202
  addSkipped(skipped, "captions", "captions.json is absent");
200
203
  }
204
+ validateCaptionTrackDeclaration(rawEdit, captionsState.value, findings);
201
205
 
202
206
  const reviewState = await readOptionalJson(
203
207
  paths.reviewPath,
@@ -424,6 +428,22 @@ function validateTransitionLayerEvacuations(rawEdit, internalEdit, findings) {
424
428
  }
425
429
  }
426
430
 
431
+ export function validateCaptionTrackDeclaration(rawEdit, captionsRoot, findings) {
432
+ if (!isRecord(rawEdit) || rawEdit.version !== 2
433
+ || !captionsHaveRenderableCues(captionsRoot)) return;
434
+ const declared = Array.isArray(rawEdit.tracks) && rawEdit.tracks.some(
435
+ track => isRecord(track) && isRecord(track.content)
436
+ && track.content.from === "captions.json",
437
+ );
438
+ if (declared) return;
439
+ addFinding(findings, {
440
+ severity: "warning",
441
+ check: "v2.captions-track-undeclared",
442
+ message: 'captions.json に描画対象 cue がありますが字幕トラックが未宣言です。現状は暗黙補完で表示自体はされています。tracks[] に { "id": "captions", "lane": "visual", "content": { "from": "captions.json" } } を追加してください。',
443
+ path: "edit.json#tracks",
444
+ });
445
+ }
446
+
427
447
  function projectAudioForLint(internalEdit) {
428
448
  const sfx = [];
429
449
  const narration = [];
@@ -96,6 +96,16 @@ export declare function scheduleCaptionFragments(start: number, end: number, fra
96
96
  text: string;
97
97
  }>;
98
98
  export declare function mergeCaptionDisplayStyles(base: unknown, override: unknown): UnknownRecord | undefined;
99
+ /**
100
+ * text_anchor(9 点)+ position(0..1 相対)→ プレート配置の CSS 変数。単一定義
101
+ * (プレビュー = shell captionTextStyleVars / 書き出し = render-cut captions.mjs の両消費者が
102
+ * これを使う — 2026-08-26 akari-reel 実機: プレビュー側だけ text_anchor/position を落として
103
+ * 明示位置付き字幕が既定の下段 7% に出る「出力とプレビューの位置不一致」の再発防止)。
104
+ * position 未指定なら anchor は zone 相当の縁寄せとして効く。position 指定時は
105
+ * その座標へ anchor の縦成分(t/m/b)を合わせる(m は 100% を超えないよう近似で top 配置)。
106
+ * 不正な anchor / vertical_align は未宣言として無視する(書き込み時検証済みが前提の防御)。
107
+ */
108
+ export declare function captionAnchorPositionVars(anchorValue: unknown, positionValue: unknown, verticalAlignValue: unknown): Record<string, string>;
99
109
  export declare function resolveCaptionStyleForOutput(style: UnknownRecord, output: {
100
110
  width: number;
101
111
  height: number;
@@ -14,6 +14,7 @@ exports.validateCaptionTextStyle = validateCaptionTextStyle;
14
14
  exports.splitCaptionFragments = splitCaptionFragments;
15
15
  exports.scheduleCaptionFragments = scheduleCaptionFragments;
16
16
  exports.mergeCaptionDisplayStyles = mergeCaptionDisplayStyles;
17
+ exports.captionAnchorPositionVars = captionAnchorPositionVars;
17
18
  exports.resolveCaptionStyleForOutput = resolveCaptionStyleForOutput;
18
19
  exports.formatCssNumber = formatCssNumber;
19
20
  exports.CAPTION_DISPLAY_SCHEMA = 'caption-layout/v1';
@@ -714,6 +715,57 @@ function mergeCaptionDisplayStyles(base, override) {
714
715
  fail('STYLE_LAYOUT_CONFLICT', 'merged caption text style cannot contain both zone and layout');
715
716
  return merged;
716
717
  }
718
+ /**
719
+ * text_anchor(9 点)+ position(0..1 相対)→ プレート配置の CSS 変数。単一定義
720
+ * (プレビュー = shell captionTextStyleVars / 書き出し = render-cut captions.mjs の両消費者が
721
+ * これを使う — 2026-08-26 akari-reel 実機: プレビュー側だけ text_anchor/position を落として
722
+ * 明示位置付き字幕が既定の下段 7% に出る「出力とプレビューの位置不一致」の再発防止)。
723
+ * position 未指定なら anchor は zone 相当の縁寄せとして効く。position 指定時は
724
+ * その座標へ anchor の縦成分(t/m/b)を合わせる(m は 100% を超えないよう近似で top 配置)。
725
+ * 不正な anchor / vertical_align は未宣言として無視する(書き込み時検証済みが前提の防御)。
726
+ */
727
+ function captionAnchorPositionVars(anchorValue, positionValue, verticalAlignValue) {
728
+ const anchor = typeof anchorValue === 'string' && CAPTION_TEXT_ANCHOR_VALUES.has(anchorValue)
729
+ ? anchorValue : undefined;
730
+ const position = isRecord(positionValue) ? positionValue : undefined;
731
+ const verticalAlign = typeof verticalAlignValue === 'string'
732
+ && CAPTION_VERTICAL_ALIGN_VALUES.has(verticalAlignValue) ? verticalAlignValue : undefined;
733
+ if (!anchor && !position && !verticalAlign)
734
+ return {};
735
+ const vars = {};
736
+ const vertical = anchor
737
+ ? anchor[0]
738
+ : verticalAlign === 'top' ? 't' : verticalAlign === 'middle' ? 'm' : 'b';
739
+ const horizontal = anchor ? anchor[1] : 'c';
740
+ if (typeof position?.y === 'number' && Number.isFinite(position.y)) {
741
+ const clamped = Math.min(1, Math.max(0, position.y));
742
+ vars['--caption-top'] = `${Math.round(clamped * 10000) / 100}%`;
743
+ vars['--caption-bottom'] = 'auto';
744
+ }
745
+ else if (anchor || verticalAlign) {
746
+ vars['--caption-top'] = vertical === 't' ? '7%' : vertical === 'm' ? '0' : 'auto';
747
+ vars['--caption-bottom'] = vertical === 'b' ? '7%' : vertical === 'm' ? '0' : 'auto';
748
+ if (vertical === 'm')
749
+ vars['--caption-justify-content'] = 'center';
750
+ }
751
+ if (typeof position?.x === 'number' && Number.isFinite(position.x)) {
752
+ const clamped = Math.min(1, Math.max(0, position.x));
753
+ vars['--caption-left'] = `${Math.round(clamped * 10000) / 100}%`;
754
+ vars['--caption-right'] = '4%';
755
+ vars['--caption-align-items'] = 'flex-start';
756
+ vars['--caption-line-margin'] = '0';
757
+ }
758
+ else if (anchor) {
759
+ vars['--caption-left'] = '4%';
760
+ vars['--caption-right'] = '4%';
761
+ vars['--caption-align-items'] = horizontal === 'l'
762
+ ? 'flex-start' : horizontal === 'r' ? 'flex-end' : 'center';
763
+ vars['--caption-text-align'] = horizontal === 'l' ? 'left' : horizontal === 'r' ? 'right' : 'center';
764
+ vars['--caption-line-margin'] = '0';
765
+ vars['--caption-line-max-width'] = '100%';
766
+ }
767
+ return vars;
768
+ }
717
769
  function resolveCaptionStyleForOutput(style, output) {
718
770
  const vars = {};
719
771
  let layout;
@@ -759,6 +811,11 @@ function resolveCaptionStyleForOutput(style, output) {
759
811
  vars['--plate-radius'] = `${formatCssNumber(style.background.radius_px * scale)}px`;
760
812
  vars['--plate-block-radius'] = `${formatCssNumber(style.background.radius_px * scale)}px`;
761
813
  }
814
+ // layout(reference-pixel)は px 座標で left/right/bottom を確定済みなので anchor と併用しない
815
+ // (zone + layout は mergeCaptionDisplayStyles が既に拒否している)。
816
+ if (layout === undefined) {
817
+ Object.assign(vars, captionAnchorPositionVars(style.text_anchor, style.position, style.vertical_align));
818
+ }
762
819
  return { vars, ...(layout ? { layout } : {}) };
763
820
  }
764
821
  function resolveReferencePixelLayout(value, output) {
@@ -17,5 +17,6 @@ export * from './track-order';
17
17
  export * from './track-transition-compatibility';
18
18
  export * from './cut-adjacency';
19
19
  export * from './transition-vocabulary';
20
+ export * from './transition-visual';
20
21
  export { parseEdit } from './migrate/legacy-parse';
21
22
  export { LegacyEditVersionError } from './migrate/error';
@@ -34,6 +34,7 @@ __exportStar(require("./track-order"), exports);
34
34
  __exportStar(require("./track-transition-compatibility"), exports);
35
35
  __exportStar(require("./cut-adjacency"), exports);
36
36
  __exportStar(require("./transition-vocabulary"), exports);
37
+ __exportStar(require("./transition-visual"), exports);
37
38
  // Legacy parser implementation lives in the frozen migration unit. This re-export keeps
38
39
  // text-surgery consumers source-compatible while preventing legacy knowledge from returning
39
40
  // to edit-store.ts.
@@ -129,7 +129,7 @@ function readV2Internal(raw) {
129
129
  // 同時表示も表現できない。先に全 visual media アイテムを横断して区間交差を求め、
130
130
  // 参加したアイテムを各トラックの build へ同じ集合として渡す。これにより preview / render
131
131
  // の両方が、トラックごとに独立した要素を合成できる layers 経路を選ぶ。
132
- const overlappingItemIds = computeOverlappingItemIds(edit.tracks.flatMap(track => 'items' in track && track.lane === 'visual' ? [track.items] : []));
132
+ const overlappingItemIds = computeOverlappingItemIds(edit.tracks.flatMap(track => 'items' in track && track.lane === 'visual' ? [track.items] : []), pathOf);
133
133
  const tracks = edit.tracks.map(track => {
134
134
  // P0 2026-08-21 render-path-unification (実測で発覚): 'cuts' 経路(concat チェーン)は
135
135
  // 同じトラック上の複数アイテムを「順番に連結される別セグメント」として扱う構造的前提を
@@ -321,7 +321,7 @@ function needsLayersEngine(item, chromaKeyOf, hasOverlappingSibling = false) {
321
321
  return true;
322
322
  return false;
323
323
  }
324
- function analyzeOverlappingItems(itemGroups) {
324
+ function analyzeOverlappingItems(itemGroups, pathOf) {
325
325
  const overlapping = new Set();
326
326
  const crossTrackEvacuations = [];
327
327
  const entries = itemGroups.flatMap((group, trackIndex) => group.items.map(item => ({ item, trackIndex, trackId: group.trackId })));
@@ -347,7 +347,7 @@ function analyzeOverlappingItems(itemGroups) {
347
347
  const upperIsA = aTrackIndex > bTrackIndex;
348
348
  const upper = upperIsA ? a : b;
349
349
  const lower = upperIsA ? b : a;
350
- if (needsCrossTrackLayers(upper)) {
350
+ if (needsCrossTrackLayers(upper, pathOf)) {
351
351
  overlapping.add(upper.id);
352
352
  crossTrackEvacuations.push({
353
353
  itemId: upper.id,
@@ -363,11 +363,11 @@ function analyzeOverlappingItems(itemGroups) {
363
363
  }
364
364
  return { itemIds: overlapping, crossTrackEvacuations };
365
365
  }
366
- function computeOverlappingItemIds(itemGroups) {
366
+ function computeOverlappingItemIds(itemGroups, pathOf) {
367
367
  return analyzeOverlappingItems(itemGroups.map((items, index) => ({
368
368
  items,
369
369
  trackId: String(index)
370
- }))).itemIds;
370
+ })), pathOf).itemIds;
371
371
  }
372
372
  /**
373
373
  * 別 visual track との重なりが原因で upper item が layers へ退避される組を返す。
@@ -375,14 +375,25 @@ function computeOverlappingItemIds(itemGroups) {
375
375
  */
376
376
  function findCrossTrackLayerEvacuations(edit) {
377
377
  const parsed = (0, edit_v2_1.readEditV2)(edit);
378
+ const pathOf = (id) => parsed.sources.find(entry => entry.id === id)?.path;
378
379
  return analyzeOverlappingItems(parsed.tracks.flatMap(track => track.lane === 'visual' && 'items' in track
379
380
  ? [{ items: track.items, trackId: track.id }]
380
- : [])).crossTrackEvacuations;
381
+ : []), pathOf).crossTrackEvacuations;
381
382
  }
382
383
  // cuts の winner-take-all が下段を隠してよいのは、上段が全画面を不透明に覆う場合だけ。
383
384
  // transform の単位元を明示しただけなら従来経路を維持する。crop / 半透明 / keyframes は、
384
385
  // 現在または途中フレームで下段が見える可能性があるため宣言の存在だけで layers へ退避する。
385
- function needsCrossTrackLayers(item) {
386
+ // 加えて、アルファを運べるコンテナ(webm / mov — 本製品のマット生成パイプラインの出力形式)は
387
+ // 宣言からは不透明を証明できないため、単位元 transform でも layers へ退避する。単位元 transform の
388
+ // 全画面アルファ webm(例: mask-top.webm)が cuts に残ると、プレビューの平坦化で
389
+ // マットが本編の勝者になり「ソース範囲がほぼ同一の縮退セグメント群」を作って
390
+ // 再生ヘッドが境界で巻き戻る(2026-08-26 akari-reel 実機・15.5s→11.2s ループの真因)。
391
+ // 静止画(png 等)は退避先の layers 経路が video 前提のため対象外(従来どおり cuts に残す)。
392
+ const ALPHA_CAPABLE_MEDIA_SOURCE_PATTERN = /\.(webm|mov)$/iu;
393
+ function isAlphaCapableMediaSourcePath(path) {
394
+ return typeof path === 'string' && ALPHA_CAPABLE_MEDIA_SOURCE_PATTERN.test(path);
395
+ }
396
+ function needsCrossTrackLayers(item, pathOf) {
386
397
  const transform = item.transform;
387
398
  return (transform?.scale !== undefined && transform.scale !== 1)
388
399
  || (transform?.x !== undefined && transform.x !== 0)
@@ -390,7 +401,8 @@ function needsCrossTrackLayers(item) {
390
401
  || (transform?.rotate !== undefined && transform.rotate !== 0)
391
402
  || item.crop !== undefined
392
403
  || (item.opacity !== undefined && item.opacity < 1)
393
- || item.keyframes !== undefined;
404
+ || item.keyframes !== undefined
405
+ || (item.source.kind === 'media' && isAlphaCapableMediaSourcePath(pathOf?.(item.source.src)));
394
406
  }
395
407
  function nextRef(counters, kind) {
396
408
  const ref = counters.get(kind) ?? 0;
@@ -46,6 +46,8 @@ export interface MigrationBlocked {
46
46
  blockers: string[];
47
47
  }
48
48
  export declare function detectEditVersion(raw: unknown): number | undefined;
49
+ /** captions.json / legacy captions[] に描画対象の cue が 1 件以上あるかを判定する。 */
50
+ export declare function captionsHaveRenderableCues(root: unknown): boolean;
49
51
  export declare function migrateEditToV2(raw: unknown, options?: {
50
52
  hasCaptions?: boolean;
51
53
  }): MigrateResult;
@@ -9,6 +9,7 @@
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.parseEdit = exports.LegacyEditVersionError = void 0;
11
11
  exports.detectEditVersion = detectEditVersion;
12
+ exports.captionsHaveRenderableCues = captionsHaveRenderableCues;
12
13
  exports.migrateEditToV2 = migrateEditToV2;
13
14
  exports.planMigration = planMigration;
14
15
  exports.applyMigration = applyMigration;
@@ -42,6 +43,13 @@ function detectEditVersion(raw) {
42
43
  return isRecord(raw) && typeof raw.version === 'number' && Number.isFinite(raw.version)
43
44
  ? raw.version : undefined;
44
45
  }
46
+ /** captions.json / legacy captions[] に描画対象の cue が 1 件以上あるかを判定する。 */
47
+ function captionsHaveRenderableCues(root) {
48
+ const cues = Array.isArray(root)
49
+ ? root
50
+ : isRecord(root) && Array.isArray(root.captions) ? root.captions : [];
51
+ return cues.some(cue => isRecord(cue) && (nonEmpty(cue.text) || nonEmpty(cue.display_text)));
52
+ }
45
53
  function migrateEditToV2(raw, options = {}) {
46
54
  const version = detectEditVersion(raw);
47
55
  if (version === 2) {
@@ -361,7 +369,7 @@ function migrateEditToV2(raw, options = {}) {
361
369
  }
362
370
  if (blockers.length > 0)
363
371
  return { ok: false, version, blockers };
364
- const hasCaptions = options.hasCaptions === true || Array.isArray(raw.captions);
372
+ const hasCaptions = options.hasCaptions === true || captionsHaveRenderableCues(raw.captions);
365
373
  const trackDefs = readTrackDefs(raw.timeline, pending, audioTrackRefs.all, hasCaptions, blockers);
366
374
  if (blockers.length > 0)
367
375
  return { ok: false, version, blockers };
@@ -382,13 +390,6 @@ function migrateEditToV2(raw, options = {}) {
382
390
  blockers.push(`timeline.tracks に ${entry.kind} ref=${entry.ref} の行がありません。`);
383
391
  }
384
392
  }
385
- // captions は pending に乗らないため上のループでは検出できない。cuts/overlays/layers と
386
- // 同じ「黙って落とさない」安全網を captions にも適用する(P0 2026-08-21
387
- // track-z-undeclared-kind: timeline.tracks を部分宣言し captions 行だけ書き忘れると、
388
- // captions トラックが変換後の tracks[] から跡形もなく消え、字幕が無警告で失われていた)。
389
- if (hasCaptions && raw.timeline !== undefined && !trackDefs.some(def => def.kind === 'captions')) {
390
- blockers.push('timeline.tracks に captions 行がありません。captions.json / captions[] が存在するため、このまま変換すると字幕が失われます。');
391
- }
392
393
  if (blockers.length > 0)
393
394
  return { ok: false, version, blockers };
394
395
  const doc = {
@@ -417,6 +418,11 @@ function migrateEditToV2(raw, options = {}) {
417
418
  });
418
419
  if (raw.thumbnail !== undefined)
419
420
  changes.push({ path: 'thumbnail', note: 'サムネイル参照を変更せず持ち越し' });
421
+ if (hasCaptions && !timelineDeclaresCaptions(raw.timeline))
422
+ changes.push({
423
+ path: 'tracks[]',
424
+ note: '字幕トラック宣言(captions.json 参照)を tracks[] 末尾(最上段)へ合成'
425
+ });
420
426
  if (pending.reduce((sum, entry) => sum + tracks.filter(track => 'items' in track
421
427
  && track.items.some(item => item === entry.item)).length, 0) !== pending.length) {
422
428
  return { ok: false, version, blockers: ['変換後の tracks[] が pending item を一意に保持していません。'] };
@@ -441,15 +447,11 @@ function planMigration(projectRoot, editPath, text, options = {}) {
441
447
  catch (error) {
442
448
  return { ok: false, version: -1, blockers: [`edit.json を JSON として読めません: ${messageOf(error)}`] };
443
449
  }
444
- // 呼び出し元(akari-preview-service.ts の prepareLegacyEdit / resolveCaptionDisplay)は
445
- // hasCaptions を渡さない。captions.json は常に edit.json と同じディレクトリに置かれる規約
446
- // (akari-preview-captions.ts の locatePreviewCaptions) なので、ここで実在チェックして
447
- // 補う。呼び出し元が明示的に hasCaptions を渡した場合はそちらを優先する
448
- // (P0 2026-08-21 track-z-undeclared-kind 追補: これが無いと migrateEditToV2 側の
449
- // captions 安全網が実プレビュー経路では一度も発火せず、字幕消失バグが直らないまま残っていた)。
450
+ // annotations の書き込み経路も projectRoot 付きで planMigration を呼ぶため、ここでの解決だけで
451
+ // CLI と同じ cue 判定が適用され、呼び出し元への追加配線は要らない。
450
452
  const resolvedProjectRoot = (0, path_1.resolve)(projectRoot);
451
453
  const captionsPath = (0, path_1.join)(resolvedProjectRoot, 'captions.json');
452
- const hasCaptions = options.hasCaptions ?? (0, node_fs_1.existsSync)(captionsPath);
454
+ const hasCaptions = options.hasCaptions ?? readCaptionsHaveRenderableCues(captionsPath);
453
455
  const migrated = migrateEditToV2(raw, { hasCaptions });
454
456
  if ('blockers' in migrated) {
455
457
  return { ok: false, version: migrated.version, blockers: migrated.blockers };
@@ -555,6 +557,9 @@ function readTrackDefs(timeline, pending, audioRefs, hasCaptions, blockers) {
555
557
  ...(typeof value.label === 'string' ? { label: value.label } : {})
556
558
  }];
557
559
  });
560
+ if (hasCaptions && !declared.some(def => def.kind === 'captions')) {
561
+ declared.push({ id: uniqueId('captions', ids), kind: 'captions' });
562
+ }
558
563
  return orderedTrackDefs(declared, audioRefs, hasCaptions, blockers);
559
564
  }
560
565
  const defs = [];
@@ -587,6 +592,18 @@ function orderedTrackDefs(declared, audioRefs, hasCaptions, blockers) {
587
592
  }
588
593
  return [...audio, ...visual];
589
594
  }
595
+ function timelineDeclaresCaptions(timeline) {
596
+ return isRecord(timeline) && Array.isArray(timeline.tracks)
597
+ && timeline.tracks.some(track => isRecord(track) && track.kind === 'captions');
598
+ }
599
+ function readCaptionsHaveRenderableCues(captionsPath) {
600
+ try {
601
+ return captionsHaveRenderableCues(JSON.parse((0, node_fs_1.readFileSync)(captionsPath, 'utf8')));
602
+ }
603
+ catch {
604
+ return false;
605
+ }
606
+ }
590
607
  function legacyAudioTrackRefs(value) {
591
608
  const audio = isRecord(value) ? value : undefined;
592
609
  if (!audio)
@@ -0,0 +1,23 @@
1
+ import type { TransitionPreviewKind } from './transition-vocabulary';
2
+ export interface PreviewTransitionVisual {
3
+ progress: number;
4
+ engine: 'none' | 'directional-blur' | 'pixelize' | 'noise-dissolve';
5
+ blurStdDeviationRatio: number;
6
+ pixelBlockRatio: number;
7
+ dissolveVisibleRatio: number;
8
+ outgoingOpacity: number;
9
+ incomingOpacity: number;
10
+ incomingClipPath: string;
11
+ outgoingTransform: string;
12
+ incomingTransform: string;
13
+ outgoingMask: string;
14
+ incomingMask: string;
15
+ outgoingFilter: string;
16
+ incomingFilter: string;
17
+ plateOpacity: number;
18
+ plateColor: string;
19
+ zSwap: boolean;
20
+ fallbackLabel: string;
21
+ }
22
+ /** Shell の「位置と動きを掴む」プレビュー用。ffmpeg xfade と画素一致は狙わない。 */
23
+ export declare function computeTransitionVisual(previewKind: TransitionPreviewKind | string, rawProgress: number, fallbackName?: string): PreviewTransitionVisual;
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeTransitionVisual = computeTransitionVisual;
4
+ /** Shell の「位置と動きを掴む」プレビュー用。ffmpeg xfade と画素一致は狙わない。 */
5
+ function computeTransitionVisual(previewKind, rawProgress, fallbackName = '') {
6
+ const clamp01 = (value) => Math.max(0, Math.min(1, value));
7
+ const progress = clamp01(Number.isFinite(rawProgress) ? rawProgress : 0);
8
+ const mid = 1 - Math.abs(2 * progress - 1);
9
+ const percent = (value) => `${value * 100}%`;
10
+ const translateX = (value) => `translateX(${percent(value)})`;
11
+ const translateY = (value) => `translateY(${percent(value)})`;
12
+ const base = {
13
+ progress,
14
+ engine: 'none',
15
+ blurStdDeviationRatio: 0,
16
+ pixelBlockRatio: 0,
17
+ dissolveVisibleRatio: 0,
18
+ outgoingOpacity: 1,
19
+ incomingOpacity: 1,
20
+ incomingClipPath: 'none',
21
+ outgoingTransform: '',
22
+ incomingTransform: '',
23
+ outgoingMask: 'none',
24
+ incomingMask: 'none',
25
+ outgoingFilter: 'none',
26
+ incomingFilter: 'none',
27
+ plateOpacity: 0,
28
+ plateColor: 'transparent',
29
+ zSwap: false,
30
+ fallbackLabel: ''
31
+ };
32
+ const cross = () => ({
33
+ ...base,
34
+ outgoingOpacity: 1 - progress,
35
+ incomingOpacity: progress
36
+ });
37
+ if (previewKind === 'blur') {
38
+ return { ...cross(), engine: 'directional-blur', blurStdDeviationRatio: mid * 0.075 };
39
+ }
40
+ if (previewKind === 'pixelize') {
41
+ return { ...cross(), engine: 'pixelize', pixelBlockRatio: mid / 22 };
42
+ }
43
+ if (previewKind === 'dissolve') {
44
+ return { ...base, engine: 'noise-dissolve', dissolveVisibleRatio: progress };
45
+ }
46
+ if (previewKind === 'fade')
47
+ return cross();
48
+ if (previewKind === 'fade-black' || previewKind === 'fade-white') {
49
+ return {
50
+ ...cross(),
51
+ plateOpacity: clamp01(Math.min(progress / 0.18, (1 - progress) / 0.7)),
52
+ plateColor: previewKind === 'fade-white' ? '#fff' : '#000'
53
+ };
54
+ }
55
+ if (previewKind === 'fade-grays') {
56
+ const filter = `grayscale(${mid})`;
57
+ return { ...cross(), outgoingFilter: filter, incomingFilter: filter };
58
+ }
59
+ const hidden = 1 - progress;
60
+ if (previewKind === 'wipe-left')
61
+ return { ...base, incomingClipPath: `inset(0 0 0 ${percent(hidden)})` };
62
+ if (previewKind === 'wipe-right')
63
+ return { ...base, incomingClipPath: `inset(0 ${percent(hidden)} 0 0)` };
64
+ if (previewKind === 'wipe-up')
65
+ return { ...base, incomingClipPath: `inset(${percent(hidden)} 0 0 0)` };
66
+ if (previewKind === 'wipe-down')
67
+ return { ...base, incomingClipPath: `inset(0 0 ${percent(hidden)} 0)` };
68
+ if (previewKind === 'slide-left') {
69
+ return { ...base, outgoingTransform: translateX(-progress), incomingTransform: translateX(hidden) };
70
+ }
71
+ if (previewKind === 'slide-right') {
72
+ return { ...base, outgoingTransform: translateX(progress), incomingTransform: translateX(-hidden) };
73
+ }
74
+ if (previewKind === 'slide-up') {
75
+ return { ...base, outgoingTransform: translateY(-progress), incomingTransform: translateY(hidden) };
76
+ }
77
+ if (previewKind === 'slide-down') {
78
+ return { ...base, outgoingTransform: translateY(progress), incomingTransform: translateY(-hidden) };
79
+ }
80
+ if (previewKind === 'cover-left')
81
+ return { ...base, incomingTransform: translateX(hidden) };
82
+ if (previewKind === 'cover-right')
83
+ return { ...base, incomingTransform: translateX(-hidden) };
84
+ if (previewKind === 'cover-up')
85
+ return { ...base, incomingTransform: translateY(hidden) };
86
+ if (previewKind === 'cover-down')
87
+ return { ...base, incomingTransform: translateY(-hidden) };
88
+ if (previewKind === 'reveal-left') {
89
+ return { ...base, outgoingTransform: translateX(-progress), zSwap: true };
90
+ }
91
+ if (previewKind === 'reveal-right') {
92
+ return { ...base, outgoingTransform: translateX(progress), zSwap: true };
93
+ }
94
+ if (previewKind === 'reveal-up') {
95
+ return { ...base, outgoingTransform: translateY(-progress), zSwap: true };
96
+ }
97
+ if (previewKind === 'reveal-down') {
98
+ return { ...base, outgoingTransform: translateY(progress), zSwap: true };
99
+ }
100
+ if (previewKind === 'circle-open') {
101
+ const c = progress * 170 - 35;
102
+ return {
103
+ ...base,
104
+ incomingMask: `radial-gradient(circle farthest-corner, #000 ${c - 35}%, transparent ${c + 35}%)`
105
+ };
106
+ }
107
+ if (previewKind === 'circle-close') {
108
+ const c = (1 - progress) * 170 - 35;
109
+ return {
110
+ ...base,
111
+ outgoingMask: `radial-gradient(circle farthest-corner, #000 ${c - 35}%, transparent ${c + 35}%)`,
112
+ zSwap: true
113
+ };
114
+ }
115
+ if (previewKind === 'radial') {
116
+ const c = progress * 424 - 32;
117
+ return {
118
+ ...base,
119
+ incomingMask: `conic-gradient(from 0deg, #000 ${c - 16}deg, transparent ${c + 16}deg)`
120
+ };
121
+ }
122
+ if (previewKind === 'zoom-in') {
123
+ return {
124
+ ...base,
125
+ outgoingOpacity: progress < 0.6 ? 1 : 1 - (progress - 0.6) / 0.4,
126
+ outgoingTransform: `scale(${1 + 1.5 * progress})`,
127
+ outgoingFilter: `blur(${6 * progress}px)`,
128
+ zSwap: true
129
+ };
130
+ }
131
+ if (previewKind === 'squeeze-h') {
132
+ return { ...base, outgoingTransform: `scaleY(${1 - progress})`, zSwap: true };
133
+ }
134
+ if (previewKind === 'squeeze-v') {
135
+ return { ...base, outgoingTransform: `scaleX(${1 - progress})`, zSwap: true };
136
+ }
137
+ return {
138
+ ...cross(),
139
+ fallbackLabel: `${fallbackName || previewKind} — プレビュー近似なし`
140
+ };
141
+ }
@@ -8,6 +8,8 @@
8
8
  */
9
9
  export * from './timeline-map';
10
10
  export * from './caption-window';
11
+ export * from './transition-vocabulary';
12
+ export * from './transition-visual';
11
13
  /** Browser selection is timeline-domain only. Segmentation stays in the Node caller. */
12
14
  export declare function findActiveResolvedCaption<T extends {
13
15
  start: number;