akari-video 0.1.60 → 0.1.62
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/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/edit-store/lib/audio-ownership.d.ts +10 -0
- package/vendor/packages/edit-store/lib/audio-ownership.js +8 -0
- package/vendor/packages/edit-store/lib/audio-schedule.d.ts +24 -1
- package/vendor/packages/edit-store/lib/audio-schedule.js +59 -15
- package/vendor/packages/edit-store/lib/internal-model.js +2 -1
- package/vendor/packages/edit-store/lib/webview-kernel.js +53 -15
- package/vendor/packages/schemas/edit.schema.json +2 -0
- package/vendor/packages/schemas/test/layer-audio.test.mjs +25 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.62",
|
|
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.
|
|
3
|
+
"version": "0.1.62",
|
|
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": [
|
|
@@ -11,3 +11,13 @@ export declare function isCutAudioAudible(cut: {
|
|
|
11
11
|
}, track?: {
|
|
12
12
|
muted?: unknown;
|
|
13
13
|
}): boolean;
|
|
14
|
+
/** Video layers own embedded speech even when their pixels are hidden or transformed. */
|
|
15
|
+
export declare function isLayerAudioAudible(layer: {
|
|
16
|
+
kind?: unknown;
|
|
17
|
+
src?: unknown;
|
|
18
|
+
isImage?: unknown;
|
|
19
|
+
audio?: unknown;
|
|
20
|
+
mute?: unknown;
|
|
21
|
+
}, track?: {
|
|
22
|
+
muted?: unknown;
|
|
23
|
+
}): boolean;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isAudioItemAudible = isAudioItemAudible;
|
|
4
4
|
exports.isCutAudioAudible = isCutAudioAudible;
|
|
5
|
+
exports.isLayerAudioAudible = isLayerAudioAudible;
|
|
5
6
|
/** Audibility belongs to the owning track and item, independently of its speech role. */
|
|
6
7
|
function isAudioItemAudible(track, item) {
|
|
7
8
|
return track?.muted !== true && item?.mute !== true;
|
|
@@ -10,3 +11,10 @@ function isAudioItemAudible(track, item) {
|
|
|
10
11
|
function isCutAudioAudible(cut, track) {
|
|
11
12
|
return cut.audio !== false && isAudioItemAudible(track, cut);
|
|
12
13
|
}
|
|
14
|
+
/** Video layers own embedded speech even when their pixels are hidden or transformed. */
|
|
15
|
+
function isLayerAudioAudible(layer, track) {
|
|
16
|
+
return layer.kind === 'video' && layer.isImage !== true
|
|
17
|
+
&& typeof layer.src === 'string' && layer.src.length > 0
|
|
18
|
+
&& !/\.(?:png|jpe?g|webp|bmp|gif|svg)(?:[?#].*)?$/iu.test(layer.src)
|
|
19
|
+
&& isCutAudioAudible(layer, track);
|
|
20
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DuckInterval } from './ducking';
|
|
2
|
-
import type
|
|
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;
|
|
@@ -138,4 +138,27 @@ export declare function buildWebAudioSchedule(input: WebAudioScheduleInput): Web
|
|
|
138
138
|
*/
|
|
139
139
|
export declare function projectSpeechDeclarations(cuts: readonly WebAudioSpeechCut[], options: {
|
|
140
140
|
fps: number;
|
|
141
|
+
layers?: readonly WebAudioSpeechLayer[];
|
|
141
142
|
}): WebAudioSpeechDeclaration[];
|
|
143
|
+
/** Layer timing is output time; only moving source regions supply speech. */
|
|
144
|
+
export interface WebAudioSpeechLayer {
|
|
145
|
+
id?: string;
|
|
146
|
+
kind?: string;
|
|
147
|
+
src?: string;
|
|
148
|
+
isImage?: boolean;
|
|
149
|
+
t: number;
|
|
150
|
+
duration: number;
|
|
151
|
+
in?: number;
|
|
152
|
+
speed?: number;
|
|
153
|
+
track?: number;
|
|
154
|
+
audio?: boolean;
|
|
155
|
+
mute?: unknown;
|
|
156
|
+
gain_db?: unknown;
|
|
157
|
+
freeze?: WebAudioSpeechCut['freeze'];
|
|
158
|
+
transition_out?: WebAudioSpeechCut['transition_out'];
|
|
159
|
+
}
|
|
160
|
+
export declare function projectLayerSpeechDeclarations(layers: readonly WebAudioSpeechLayer[], options: {
|
|
161
|
+
fps: number;
|
|
162
|
+
}): Array<WebAudioSpeechDeclaration & {
|
|
163
|
+
scope: 'layers';
|
|
164
|
+
}>;
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildWebAudioSchedule = buildWebAudioSchedule;
|
|
4
4
|
exports.projectSpeechDeclarations = projectSpeechDeclarations;
|
|
5
|
+
exports.projectLayerSpeechDeclarations = projectLayerSpeechDeclarations;
|
|
5
6
|
const audio_ownership_1 = require("./audio-ownership");
|
|
6
7
|
const ducking_1 = require("./ducking");
|
|
7
8
|
const envelope_1 = require("./envelope");
|
|
8
9
|
const timeline_map_1 = require("./timeline-map");
|
|
10
|
+
const edit_store_1 = require("./edit-store");
|
|
9
11
|
/**
|
|
10
12
|
* 解決済みタイムライン尺・正規化済み audio 宣言・デコード実尺を、Web Audio がそのまま
|
|
11
13
|
* 消費できる予定表へ落とす。fetch/decode/時計は扱わないため、実時間と OfflineAudioContext
|
|
@@ -305,21 +307,45 @@ function projectSpeechDeclarations(cuts, options) {
|
|
|
305
307
|
return { ...cut, out: cut.out + holdSec * speed };
|
|
306
308
|
});
|
|
307
309
|
const map = (0, timeline_map_1.buildTimelineMap)(virtualCuts, { fps });
|
|
310
|
+
// Keep every clip's window, including clips absent from the visual winner map.
|
|
311
|
+
// Match buildTimelineMap's usable-cut filtering before resolving track cursors.
|
|
312
|
+
const usable = virtualCuts.map((cut, index) => ({ cut, index })).filter(({ cut }) => Number.isFinite(cut.in) && Number.isFinite(cut.out) && cut.in < cut.out);
|
|
313
|
+
const clipWindows = new Map((0, edit_store_1.computeCutTrackSegments)(usable.map(entry => entry.cut)).map(segment => [
|
|
314
|
+
usable[segment.index].index,
|
|
315
|
+
{ start: segment.at, end: segment.end, cutTimelineStart: segment.at }
|
|
316
|
+
]));
|
|
317
|
+
for (const window of map.transitionWindows) {
|
|
318
|
+
for (const participant of [window.outgoing, window.incoming]) {
|
|
319
|
+
const clip = clipWindows.get(participant.cutIndex);
|
|
320
|
+
const cut = normalizedCuts[participant.cutIndex];
|
|
321
|
+
if (!clip || !cut || typeof participant.in !== 'number')
|
|
322
|
+
continue;
|
|
323
|
+
const speed = finitePositive(cut.speed) ? cut.speed : 1;
|
|
324
|
+
// Transition slices retain the source mapping after hidden-handle adjustment,
|
|
325
|
+
// even when another track completely covers both participants.
|
|
326
|
+
clip.cutTimelineStart = participant.outStart - (participant.in - cut.in) / speed;
|
|
327
|
+
}
|
|
328
|
+
const outgoing = clipWindows.get(window.outgoing.cutIndex);
|
|
329
|
+
const incoming = clipWindows.get(window.incoming.cutIndex);
|
|
330
|
+
if (outgoing)
|
|
331
|
+
outgoing.end = Math.max(outgoing.end, window.end);
|
|
332
|
+
// scheduleSpeech adds this incoming overlap back via crossfadeInSec. Keep the
|
|
333
|
+
// existing declaration boundary so the transition is not applied twice.
|
|
334
|
+
if (incoming)
|
|
335
|
+
incoming.start = Math.max(incoming.start, window.end);
|
|
336
|
+
}
|
|
308
337
|
const declarations = [];
|
|
309
|
-
for (const
|
|
310
|
-
|
|
311
|
-
continue;
|
|
312
|
-
const cut = normalizedCuts[segment.cutIndex];
|
|
338
|
+
for (const [cutIndex, clip] of clipWindows) {
|
|
339
|
+
const cut = normalizedCuts[cutIndex];
|
|
313
340
|
if (!cut || typeof cut.src !== 'string' || !cut.src)
|
|
314
341
|
continue;
|
|
315
342
|
if (!(0, audio_ownership_1.isCutAudioAudible)(cut))
|
|
316
343
|
continue;
|
|
317
344
|
const speed = finitePositive(cut.speed) ? cut.speed : 1;
|
|
318
|
-
const
|
|
319
|
-
const cutTimelineStart = segment.outStart - (segmentIn - cut.in) / speed;
|
|
345
|
+
const cutTimelineStart = clip.cutTimelineStart;
|
|
320
346
|
const baseDurationSec = Math.max(0, cut.out - cut.in) / speed;
|
|
321
347
|
const gainDb = speechGainDb(cut);
|
|
322
|
-
const baseId =
|
|
348
|
+
const baseId = speechBaseId(cut, cutIndex);
|
|
323
349
|
const holdSec = freezeDuration(cut.freeze);
|
|
324
350
|
if (!(holdSec > 0)) {
|
|
325
351
|
appendSpeechIntersection(declarations, {
|
|
@@ -327,8 +353,8 @@ function projectSpeechDeclarations(cuts, options) {
|
|
|
327
353
|
sourceIn: cut.in,
|
|
328
354
|
outputStart: cutTimelineStart,
|
|
329
355
|
outputEnd: cutTimelineStart + baseDurationSec,
|
|
330
|
-
|
|
331
|
-
|
|
356
|
+
clipStart: clip.start,
|
|
357
|
+
clipEnd: clip.end,
|
|
332
358
|
track: cut.track
|
|
333
359
|
});
|
|
334
360
|
continue;
|
|
@@ -340,8 +366,8 @@ function projectSpeechDeclarations(cuts, options) {
|
|
|
340
366
|
sourceIn: cut.in,
|
|
341
367
|
outputStart: cutTimelineStart,
|
|
342
368
|
outputEnd: cutTimelineStart + freezeAtSec,
|
|
343
|
-
|
|
344
|
-
|
|
369
|
+
clipStart: clip.start,
|
|
370
|
+
clipEnd: clip.end,
|
|
345
371
|
track: cut.track
|
|
346
372
|
});
|
|
347
373
|
appendSpeechIntersection(declarations, {
|
|
@@ -349,8 +375,8 @@ function projectSpeechDeclarations(cuts, options) {
|
|
|
349
375
|
sourceIn: freezeSourceIn,
|
|
350
376
|
outputStart: cutTimelineStart + freezeAtSec + holdSec,
|
|
351
377
|
outputEnd: cutTimelineStart + baseDurationSec + holdSec,
|
|
352
|
-
|
|
353
|
-
|
|
378
|
+
clipStart: clip.start,
|
|
379
|
+
clipEnd: clip.end,
|
|
354
380
|
track: cut.track
|
|
355
381
|
});
|
|
356
382
|
}
|
|
@@ -375,14 +401,32 @@ function projectSpeechDeclarations(cuts, options) {
|
|
|
375
401
|
incoming.crossfadeInSec = Math.max(incoming.crossfadeInSec ?? 0, window.duration);
|
|
376
402
|
}
|
|
377
403
|
}
|
|
404
|
+
if (options.layers?.length)
|
|
405
|
+
declarations.push(...projectLayerSpeechDeclarations(options.layers, { fps }));
|
|
378
406
|
return declarations;
|
|
379
407
|
}
|
|
408
|
+
function projectLayerSpeechDeclarations(layers, options) {
|
|
409
|
+
const cuts = layers.map((layer, index) => {
|
|
410
|
+
const speed = finitePositive(layer.speed) ? layer.speed : 1;
|
|
411
|
+
const sourceIn = finiteNonNegative(layer.in) ? layer.in : 0;
|
|
412
|
+
return {
|
|
413
|
+
...layer,
|
|
414
|
+
id: `layer-${layer.id || index}`,
|
|
415
|
+
in: sourceIn,
|
|
416
|
+
out: sourceIn + Math.max(0, layer.duration - freezeDuration(layer.freeze)) * speed,
|
|
417
|
+
at: layer.t,
|
|
418
|
+
speed,
|
|
419
|
+
audio: (0, audio_ownership_1.isLayerAudioAudible)(layer) ? undefined : false,
|
|
420
|
+
};
|
|
421
|
+
});
|
|
422
|
+
return projectSpeechDeclarations(cuts, options).map(item => ({ ...item, scope: 'layers' }));
|
|
423
|
+
}
|
|
380
424
|
function speechBaseId(cut, index) {
|
|
381
425
|
return cut && typeof cut.id === 'string' && cut.id ? cut.id : `cut-${index}`;
|
|
382
426
|
}
|
|
383
427
|
function appendSpeechIntersection(declarations, input) {
|
|
384
|
-
const atSec = Math.max(input.outputStart, input.
|
|
385
|
-
const endSec = Math.min(input.outputEnd, input.
|
|
428
|
+
const atSec = Math.max(input.outputStart, input.clipStart);
|
|
429
|
+
const endSec = Math.min(input.outputEnd, input.clipEnd);
|
|
386
430
|
if (!(endSec > atSec))
|
|
387
431
|
return;
|
|
388
432
|
const inSec = input.sourceIn + (atSec - input.outputStart) * input.speed;
|
|
@@ -1130,7 +1130,8 @@ function projectLegacyEdit(internal) {
|
|
|
1130
1130
|
audioBgm = value;
|
|
1131
1131
|
break;
|
|
1132
1132
|
case 'layers':
|
|
1133
|
-
layers.push({ index: item.legacy.index, value:
|
|
1133
|
+
layers.push({ index: item.legacy.index, value: (track.lane === 'visual' && track.muted === true
|
|
1134
|
+
? { ...value, mute: true } : value) });
|
|
1134
1135
|
break;
|
|
1135
1136
|
default:
|
|
1136
1137
|
cuts.push({
|
|
@@ -48,11 +48,13 @@ var AkariEditKernel = (() => {
|
|
|
48
48
|
findActiveResolvedCaption: () => findActiveResolvedCaption,
|
|
49
49
|
isAudioItemAudible: () => isAudioItemAudible,
|
|
50
50
|
isCutAudioAudible: () => isCutAudioAudible,
|
|
51
|
+
isLayerAudioAudible: () => isLayerAudioAudible,
|
|
51
52
|
isTransitionType: () => isTransitionType,
|
|
52
53
|
isWithinDuckInterval: () => isWithinDuckInterval,
|
|
53
54
|
mergePresetTextStyle: () => mergePresetTextStyle,
|
|
54
55
|
normalizeCaptionClock: () => normalizeCaptionClock,
|
|
55
56
|
outputToSource: () => outputToSource,
|
|
57
|
+
projectLayerSpeechDeclarations: () => projectLayerSpeechDeclarations,
|
|
56
58
|
projectSpeechDeclarations: () => projectSpeechDeclarations,
|
|
57
59
|
projectSpeechKeyIntervals: () => projectSpeechKeyIntervals,
|
|
58
60
|
resolveCaptionStylePreset: () => resolveCaptionStylePreset,
|
|
@@ -1154,6 +1156,9 @@ var AkariEditKernel = (() => {
|
|
|
1154
1156
|
function isCutAudioAudible(cut, track) {
|
|
1155
1157
|
return cut.audio !== false && isAudioItemAudible(track, cut);
|
|
1156
1158
|
}
|
|
1159
|
+
function isLayerAudioAudible(layer, track) {
|
|
1160
|
+
return layer.kind === "video" && layer.isImage !== true && typeof layer.src === "string" && layer.src.length > 0 && !/\.(?:png|jpe?g|webp|bmp|gif|svg)(?:[?#].*)?$/iu.test(layer.src) && isCutAudioAudible(layer, track);
|
|
1161
|
+
}
|
|
1157
1162
|
|
|
1158
1163
|
// src/audio-schedule.ts
|
|
1159
1164
|
function buildWebAudioSchedule(input) {
|
|
@@ -1452,18 +1457,34 @@ var AkariEditKernel = (() => {
|
|
|
1452
1457
|
return { ...cut, out: cut.out + holdSec * speed };
|
|
1453
1458
|
});
|
|
1454
1459
|
const map = buildTimelineMap(virtualCuts, { fps });
|
|
1460
|
+
const usable = virtualCuts.map((cut, index) => ({ cut, index })).filter(({ cut }) => Number.isFinite(cut.in) && Number.isFinite(cut.out) && cut.in < cut.out);
|
|
1461
|
+
const clipWindows = new Map(computeCutTrackSegments(usable.map((entry) => entry.cut)).map((segment) => [
|
|
1462
|
+
usable[segment.index].index,
|
|
1463
|
+
{ start: segment.at, end: segment.end, cutTimelineStart: segment.at }
|
|
1464
|
+
]));
|
|
1465
|
+
for (const window of map.transitionWindows) {
|
|
1466
|
+
for (const participant of [window.outgoing, window.incoming]) {
|
|
1467
|
+
const clip = clipWindows.get(participant.cutIndex);
|
|
1468
|
+
const cut = normalizedCuts[participant.cutIndex];
|
|
1469
|
+
if (!clip || !cut || typeof participant.in !== "number") continue;
|
|
1470
|
+
const speed = finitePositive(cut.speed) ? cut.speed : 1;
|
|
1471
|
+
clip.cutTimelineStart = participant.outStart - (participant.in - cut.in) / speed;
|
|
1472
|
+
}
|
|
1473
|
+
const outgoing = clipWindows.get(window.outgoing.cutIndex);
|
|
1474
|
+
const incoming = clipWindows.get(window.incoming.cutIndex);
|
|
1475
|
+
if (outgoing) outgoing.end = Math.max(outgoing.end, window.end);
|
|
1476
|
+
if (incoming) incoming.start = Math.max(incoming.start, window.end);
|
|
1477
|
+
}
|
|
1455
1478
|
const declarations = [];
|
|
1456
|
-
for (const
|
|
1457
|
-
|
|
1458
|
-
const cut = normalizedCuts[segment.cutIndex];
|
|
1479
|
+
for (const [cutIndex, clip] of clipWindows) {
|
|
1480
|
+
const cut = normalizedCuts[cutIndex];
|
|
1459
1481
|
if (!cut || typeof cut.src !== "string" || !cut.src) continue;
|
|
1460
1482
|
if (!isCutAudioAudible(cut)) continue;
|
|
1461
1483
|
const speed = finitePositive(cut.speed) ? cut.speed : 1;
|
|
1462
|
-
const
|
|
1463
|
-
const cutTimelineStart = segment.outStart - (segmentIn - cut.in) / speed;
|
|
1484
|
+
const cutTimelineStart = clip.cutTimelineStart;
|
|
1464
1485
|
const baseDurationSec = Math.max(0, cut.out - cut.in) / speed;
|
|
1465
1486
|
const gainDb = speechGainDb(cut);
|
|
1466
|
-
const baseId =
|
|
1487
|
+
const baseId = speechBaseId(cut, cutIndex);
|
|
1467
1488
|
const holdSec = freezeDuration(cut.freeze);
|
|
1468
1489
|
if (!(holdSec > 0)) {
|
|
1469
1490
|
appendSpeechIntersection(declarations, {
|
|
@@ -1474,8 +1495,8 @@ var AkariEditKernel = (() => {
|
|
|
1474
1495
|
sourceIn: cut.in,
|
|
1475
1496
|
outputStart: cutTimelineStart,
|
|
1476
1497
|
outputEnd: cutTimelineStart + baseDurationSec,
|
|
1477
|
-
|
|
1478
|
-
|
|
1498
|
+
clipStart: clip.start,
|
|
1499
|
+
clipEnd: clip.end,
|
|
1479
1500
|
track: cut.track
|
|
1480
1501
|
});
|
|
1481
1502
|
continue;
|
|
@@ -1490,8 +1511,8 @@ var AkariEditKernel = (() => {
|
|
|
1490
1511
|
sourceIn: cut.in,
|
|
1491
1512
|
outputStart: cutTimelineStart,
|
|
1492
1513
|
outputEnd: cutTimelineStart + freezeAtSec,
|
|
1493
|
-
|
|
1494
|
-
|
|
1514
|
+
clipStart: clip.start,
|
|
1515
|
+
clipEnd: clip.end,
|
|
1495
1516
|
track: cut.track
|
|
1496
1517
|
});
|
|
1497
1518
|
appendSpeechIntersection(declarations, {
|
|
@@ -1502,8 +1523,8 @@ var AkariEditKernel = (() => {
|
|
|
1502
1523
|
sourceIn: freezeSourceIn,
|
|
1503
1524
|
outputStart: cutTimelineStart + freezeAtSec + holdSec,
|
|
1504
1525
|
outputEnd: cutTimelineStart + baseDurationSec + holdSec,
|
|
1505
|
-
|
|
1506
|
-
|
|
1526
|
+
clipStart: clip.start,
|
|
1527
|
+
clipEnd: clip.end,
|
|
1507
1528
|
track: cut.track
|
|
1508
1529
|
});
|
|
1509
1530
|
}
|
|
@@ -1524,14 +1545,31 @@ var AkariEditKernel = (() => {
|
|
|
1524
1545
|
incoming.crossfadeInSec = Math.max(incoming.crossfadeInSec ?? 0, window.duration);
|
|
1525
1546
|
}
|
|
1526
1547
|
}
|
|
1548
|
+
if (options.layers?.length) declarations.push(...projectLayerSpeechDeclarations(options.layers, { fps }));
|
|
1527
1549
|
return declarations;
|
|
1528
1550
|
}
|
|
1551
|
+
function projectLayerSpeechDeclarations(layers, options) {
|
|
1552
|
+
const cuts = layers.map((layer, index) => {
|
|
1553
|
+
const speed = finitePositive(layer.speed) ? layer.speed : 1;
|
|
1554
|
+
const sourceIn = finiteNonNegative(layer.in) ? layer.in : 0;
|
|
1555
|
+
return {
|
|
1556
|
+
...layer,
|
|
1557
|
+
id: `layer-${layer.id || index}`,
|
|
1558
|
+
in: sourceIn,
|
|
1559
|
+
out: sourceIn + Math.max(0, layer.duration - freezeDuration(layer.freeze)) * speed,
|
|
1560
|
+
at: layer.t,
|
|
1561
|
+
speed,
|
|
1562
|
+
audio: isLayerAudioAudible(layer) ? void 0 : false
|
|
1563
|
+
};
|
|
1564
|
+
});
|
|
1565
|
+
return projectSpeechDeclarations(cuts, options).map((item) => ({ ...item, scope: "layers" }));
|
|
1566
|
+
}
|
|
1529
1567
|
function speechBaseId(cut, index) {
|
|
1530
1568
|
return cut && typeof cut.id === "string" && cut.id ? cut.id : `cut-${index}`;
|
|
1531
1569
|
}
|
|
1532
1570
|
function appendSpeechIntersection(declarations, input) {
|
|
1533
|
-
const atSec = Math.max(input.outputStart, input.
|
|
1534
|
-
const endSec = Math.min(input.outputEnd, input.
|
|
1571
|
+
const atSec = Math.max(input.outputStart, input.clipStart);
|
|
1572
|
+
const endSec = Math.min(input.outputEnd, input.clipEnd);
|
|
1535
1573
|
if (!(endSec > atSec)) return;
|
|
1536
1574
|
const inSec = input.sourceIn + (atSec - input.outputStart) * input.speed;
|
|
1537
1575
|
const outSec = inSec + (endSec - atSec) * input.speed;
|
|
@@ -3489,7 +3527,7 @@ var AkariEditKernel = (() => {
|
|
|
3489
3527
|
audioBgm = value;
|
|
3490
3528
|
break;
|
|
3491
3529
|
case "layers":
|
|
3492
|
-
layers.push({ index: item.legacy.index, value });
|
|
3530
|
+
layers.push({ index: item.legacy.index, value: track.lane === "visual" && track.muted === true ? { ...value, mute: true } : value });
|
|
3493
3531
|
break;
|
|
3494
3532
|
default:
|
|
3495
3533
|
cuts.push({
|
|
@@ -1368,6 +1368,8 @@
|
|
|
1368
1368
|
"duration": { "$ref": "#/$defs/positiveNumber" },
|
|
1369
1369
|
"kind": { "enum": ["baked", "video", "filter"] },
|
|
1370
1370
|
"src": { "type": "string", "minLength": 1, "pattern": "\\S" },
|
|
1371
|
+
"audio": { "type": "boolean", "default": true, "description": "Play embedded audio for video layers; ignored for other kinds." },
|
|
1372
|
+
"gain_db": { "type": "number", "minimum": -60, "maximum": 12, "default": 0 },
|
|
1371
1373
|
"filter": { "$ref": "#/$defs/layerFilter" },
|
|
1372
1374
|
"preset": { "type": "string" },
|
|
1373
1375
|
"params": { "type": "object" },
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import test from 'node:test';
|
|
4
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
5
|
+
const schema = JSON.parse(readFileSync(new URL('../edit.schema.json', import.meta.url), 'utf8'));
|
|
6
|
+
const layer = () => ({ id: 'pip', kind: 'video', src: 'pip.mov', t: 1, duration: 3 });
|
|
7
|
+
const compile = useDefaults => new Ajv2020({ strict: false, useDefaults }).compile({
|
|
8
|
+
$defs: schema.$defs, $ref: '#/$defs/layerItem',
|
|
9
|
+
});
|
|
10
|
+
test('legacy layer audio omission stays valid and defaults to audible at 0 dB', () => {
|
|
11
|
+
const value = layer(), before = JSON.stringify(value);
|
|
12
|
+
assert.equal(compile(false)(value), true);
|
|
13
|
+
assert.equal(JSON.stringify(value), before);
|
|
14
|
+
assert.equal(compile(true)(value), true);
|
|
15
|
+
assert.equal(value.audio, true);
|
|
16
|
+
assert.equal(value.gain_db, 0);
|
|
17
|
+
});
|
|
18
|
+
test('layer audio accepts booleans and gain endpoints, rejecting wrong types and ranges', () => {
|
|
19
|
+
const validate = compile(false);
|
|
20
|
+
for (const audio of [true, false]) for (const gain_db of [-60, 0, 12]) {
|
|
21
|
+
assert.equal(validate({ ...layer(), audio, gain_db }), true);
|
|
22
|
+
}
|
|
23
|
+
for (const audio of [0, null, 'false']) assert.equal(validate({ ...layer(), audio }), false);
|
|
24
|
+
for (const gain_db of [-60.1, 12.1, null, '0']) assert.equal(validate({ ...layer(), gain_db }), false);
|
|
25
|
+
});
|