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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "0.1.61",
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.61",
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: 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
+ });