akari-video 0.1.61 → 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 +23 -0
- package/vendor/packages/edit-store/lib/audio-schedule.js +19 -0
- package/vendor/packages/edit-store/lib/internal-model.js +2 -1
- package/vendor/packages/edit-store/lib/webview-kernel.js +23 -1
- 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
|
+
}
|
|
@@ -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,6 +2,7 @@
|
|
|
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");
|
|
@@ -400,8 +401,26 @@ function projectSpeechDeclarations(cuts, options) {
|
|
|
400
401
|
incoming.crossfadeInSec = Math.max(incoming.crossfadeInSec ?? 0, window.duration);
|
|
401
402
|
}
|
|
402
403
|
}
|
|
404
|
+
if (options.layers?.length)
|
|
405
|
+
declarations.push(...projectLayerSpeechDeclarations(options.layers, { fps }));
|
|
403
406
|
return declarations;
|
|
404
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
|
+
}
|
|
405
424
|
function speechBaseId(cut, index) {
|
|
406
425
|
return cut && typeof cut.id === 'string' && cut.id ? cut.id : `cut-${index}`;
|
|
407
426
|
}
|
|
@@ -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) {
|
|
@@ -1540,8 +1545,25 @@ var AkariEditKernel = (() => {
|
|
|
1540
1545
|
incoming.crossfadeInSec = Math.max(incoming.crossfadeInSec ?? 0, window.duration);
|
|
1541
1546
|
}
|
|
1542
1547
|
}
|
|
1548
|
+
if (options.layers?.length) declarations.push(...projectLayerSpeechDeclarations(options.layers, { fps }));
|
|
1543
1549
|
return declarations;
|
|
1544
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
|
+
}
|
|
1545
1567
|
function speechBaseId(cut, index) {
|
|
1546
1568
|
return cut && typeof cut.id === "string" && cut.id ? cut.id : `cut-${index}`;
|
|
1547
1569
|
}
|
|
@@ -3505,7 +3527,7 @@ var AkariEditKernel = (() => {
|
|
|
3505
3527
|
audioBgm = value;
|
|
3506
3528
|
break;
|
|
3507
3529
|
case "layers":
|
|
3508
|
-
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 });
|
|
3509
3531
|
break;
|
|
3510
3532
|
default:
|
|
3511
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
|
+
});
|