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.
- package/package.json +1 -1
- package/src/migrate-command.mjs +11 -2
- package/vendor/.akari-capability-sources.json +3 -0
- package/vendor/docs/contract-2026-07-25-project-structure-v0.md +17 -0
- package/vendor/docs/contract-2026-08-02-preview-parity.md +39 -0
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/analysis-report/package.json +4 -8
- package/vendor/packages/analysis-report/render-analysis-report.mjs +544 -0
- package/vendor/packages/analysis-report/template.html +2023 -0
- package/vendor/packages/edit-lint/src/edit-lint.mjs +20 -0
- package/vendor/packages/edit-store/lib/caption-display.d.ts +10 -0
- package/vendor/packages/edit-store/lib/caption-display.js +57 -0
- package/vendor/packages/edit-store/lib/index.d.ts +1 -0
- package/vendor/packages/edit-store/lib/index.js +1 -0
- package/vendor/packages/edit-store/lib/internal-model.js +20 -8
- package/vendor/packages/edit-store/lib/migrate/index.d.ts +2 -0
- package/vendor/packages/edit-store/lib/migrate/index.js +32 -15
- package/vendor/packages/edit-store/lib/transition-visual.d.ts +23 -0
- package/vendor/packages/edit-store/lib/transition-visual.js +141 -0
- package/vendor/packages/edit-store/lib/webview-kernel.d.ts +2 -0
- package/vendor/packages/edit-store/lib/webview-kernel.js +139 -0
- package/vendor/packages/frame-engine/README.ja.md +24 -0
- package/vendor/packages/frame-engine/README.md +24 -0
- package/vendor/packages/frame-engine/package.json +27 -0
- package/vendor/packages/preview-engine/README.md +38 -0
- 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
|
-
|
|
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 ||
|
|
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
|
-
//
|
|
445
|
-
//
|
|
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 ?? (
|
|
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;
|