@hyperframes/studio-server 0.9.16 → 0.9.17

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  probeMediaMetadata
3
- } from "./chunk-X73QTST2.js";
3
+ } from "./chunk-Y6EZUBYR.js";
4
4
  import {
5
5
  realpath
6
6
  } from "./chunk-QF7MFJJU.js";
@@ -2,11 +2,11 @@ import {
2
2
  PROXY_STILL_EXTENSION,
3
3
  PROXY_VARIANT_CONFIG,
4
4
  ffmpegHasHdrFilters
5
- } from "./chunk-NOIHVBIQ.js";
5
+ } from "./chunk-66YZA32N.js";
6
6
  import {
7
7
  probeFirstFrameColour,
8
8
  probeMediaMetadata
9
- } from "./chunk-X73QTST2.js";
9
+ } from "./chunk-Y6EZUBYR.js";
10
10
  import {
11
11
  mkdirWithinProject,
12
12
  realProjectRoot,
@@ -768,7 +768,7 @@ async function buildVideoFilter(ffmpegPath, sourcePath, metadata, keepsAlpha, bo
768
768
  const { hdrTransfer } = metadata.color;
769
769
  const toneMap = (hdrTransfer === "pq" || hdrTransfer === "hlg") && !keepsAlpha;
770
770
  if (toneMap) await ensureHdrFilters(ffmpegPath);
771
- const firstFrame = toneMap ? await probeFirstFrameColour(sourcePath) : {};
771
+ const firstFrame = toneMap ? await probeFirstFrameColour(sourcePath, metadata.color.codecName) : {};
772
772
  const evenScale = proxyScaleFilter(box);
773
773
  const pixelFormat = keepsAlpha ? "yuva420p" : "yuv420p";
774
774
  return toneMap ? [
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  PROXY_PARAMS_VERSION,
3
3
  resolveProxy
4
- } from "./chunk-B356RYG5.js";
4
+ } from "./chunk-F4AQECCT.js";
5
5
  import {
6
6
  createMediaCodecProbeCache,
7
7
  proxyVariantFor,
8
8
  recordProxyPrewarm,
9
9
  scanProjectMedia,
10
10
  shouldPrewarmProxy
11
- } from "./chunk-NOIHVBIQ.js";
11
+ } from "./chunk-66YZA32N.js";
12
12
 
13
13
  // src/helpers/mediaProxyPreview.ts
14
14
  import { resolve } from "path";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  probeAudioTrack
3
- } from "./chunk-X73QTST2.js";
3
+ } from "./chunk-Y6EZUBYR.js";
4
4
  import {
5
5
  resolveWithinProject
6
6
  } from "./chunk-QF7MFJJU.js";
@@ -277,30 +277,24 @@ function audioNormalizationPlan(referenceTrack, targetTrack) {
277
277
  }
278
278
  const referenceLufs = referenceTrack.integratedLufs + gainDb(referenceTrack.volume);
279
279
  const targetLufs = targetTrack.integratedLufs + gainDb(targetTrack.volume);
280
- const wantedGainDb = referenceLufs - targetTrack.integratedLufs;
281
- if (wantedGainDb > MAX_AUDIO_GAIN_DB + 1e-9) {
282
- throw new Error(
283
- `Matching #${targetTrack.id} needs ${wantedGainDb.toFixed(1)} dB, beyond the +${MAX_AUDIO_GAIN_DB} dB authoring ceiling. A gap this large belongs in the source file, not the mixer: mixer gain raises the noise floor with the signal. Normalize ${targetTrack.id}'s asset offline (e.g. ffmpeg loudnorm), or lower #${referenceTrack.id} instead.`
284
- );
285
- }
286
- const volume = 10 ** (wantedGainDb / 20);
287
- const projectedTruePeakDbfs = targetTrack.truePeakDbfs + wantedGainDb;
288
- if (projectedTruePeakDbfs > 0) {
289
- throw new Error(
290
- `Matching #${targetTrack.id} would clip at +${projectedTruePeakDbfs.toFixed(1)} dBFS; limit or preprocess the source first`
291
- );
292
- }
280
+ const previousGainDb = gainDb(targetTrack.volume);
281
+ const { appliedGainDb, limitedBy } = fittedGain(
282
+ referenceLufs - targetTrack.integratedLufs,
283
+ targetTrack.truePeakDbfs,
284
+ previousGainDb
285
+ );
293
286
  return {
294
287
  referenceId: referenceTrack.id,
295
288
  targetId: targetTrack.id,
296
289
  referenceLufs,
297
290
  targetLufs,
298
291
  previousVolume: targetTrack.volume,
299
- volume,
300
- gainDb: wantedGainDb,
301
- changeDb: wantedGainDb - gainDb(targetTrack.volume),
302
- projectedLufs: targetTrack.integratedLufs + wantedGainDb,
303
- projectedTruePeakDbfs
292
+ volume: 10 ** (appliedGainDb / 20),
293
+ gainDb: appliedGainDb,
294
+ changeDb: appliedGainDb - previousGainDb,
295
+ projectedLufs: targetTrack.integratedLufs + appliedGainDb,
296
+ projectedTruePeakDbfs: targetTrack.truePeakDbfs + appliedGainDb,
297
+ limitedBy
304
298
  };
305
299
  }
306
300
  function updateAudioVolume(html, id, volume) {
@@ -363,19 +357,12 @@ function absoluteLoudnessPlan(track, targetLufs = DEFAULT_TARGET_LUFS) {
363
357
  if (track.integratedLufs <= SILENCE_LUFS) {
364
358
  throw new Error(`#${track.id} is silent in the played window; there is nothing to normalize`);
365
359
  }
366
- const wantedGainDb = targetLufs - track.integratedLufs;
367
- const peakRoomDb = NORMALIZE_TRUE_PEAK_CEILING_DBFS - track.truePeakDbfs;
368
- const limits = [
369
- [null, wantedGainDb],
370
- ["gain-ceiling", MAX_AUDIO_GAIN_DB],
371
- ["true-peak", peakRoomDb]
372
- ];
373
- const [limitedBy, limitedGainDb] = limits.reduce(
374
- (lowest, candidate) => candidate[1] < lowest[1] - 1e-9 ? candidate : lowest
375
- );
376
360
  const previousGainDb = track.volume > 0 ? gainDb(track.volume) : 0;
377
- const limitCannotCutAWantedBoost = wantedGainDb > previousGainDb;
378
- const appliedGainDb = limitCannotCutAWantedBoost ? Math.max(limitedGainDb, previousGainDb) : limitedGainDb;
361
+ const { appliedGainDb, limitedBy } = fittedGain(
362
+ targetLufs - track.integratedLufs,
363
+ track.truePeakDbfs,
364
+ previousGainDb
365
+ );
379
366
  return {
380
367
  targetId: track.id,
381
368
  targetLufs,
@@ -389,6 +376,18 @@ function absoluteLoudnessPlan(track, targetLufs = DEFAULT_TARGET_LUFS) {
389
376
  limitedBy
390
377
  };
391
378
  }
379
+ function fittedGain(wantedGainDb, truePeakDbfs, previousGainDb) {
380
+ const limits = [
381
+ [null, wantedGainDb],
382
+ ["gain-ceiling", MAX_AUDIO_GAIN_DB],
383
+ ["true-peak", NORMALIZE_TRUE_PEAK_CEILING_DBFS - truePeakDbfs]
384
+ ];
385
+ const [limitedBy, limitedGainDb] = limits.reduce(
386
+ (lowest, candidate) => candidate[1] < lowest[1] - 1e-9 ? candidate : lowest
387
+ );
388
+ const appliedGainDb = wantedGainDb > previousGainDb ? Math.max(limitedGainDb, previousGainDb) : limitedGainDb;
389
+ return { appliedGainDb, limitedBy };
390
+ }
392
391
  function absoluteOutcome(plan, tolerance) {
393
392
  if (Math.abs(plan.changeDb) > tolerance) return "change";
394
393
  return Math.abs(plan.projectedLufs - plan.targetLufs) <= tolerance ? "matched" : "held";
@@ -3,7 +3,10 @@ import { execFile } from "child_process";
3
3
  import { extname } from "path";
4
4
  import {
5
5
  firstFrameColourArgs,
6
- parseFirstFrameColour
6
+ keyframeTransferArgs,
7
+ parseFirstFrameColour,
8
+ readsKeyframeTransfers,
9
+ parseKeyframeTransfers
7
10
  } from "@hyperframes/core";
8
11
  import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
9
12
  import { videoStreamHasAlpha } from "@hyperframes/parsers/media-alpha";
@@ -82,14 +85,21 @@ function classifyMediaColor(stream) {
82
85
  bitsPerRawSample: stream?.bits_per_raw_sample
83
86
  };
84
87
  }
85
- async function probeFirstFrameColour(filePath, runner = execFileRunner) {
88
+ async function probeFirstFrameColour(filePath, codec, runner = execFileRunner) {
86
89
  const ffmpegPath = findFfBinary("ffmpeg", { configuredMustExist: true }) ?? (runner === execFileRunner ? void 0 : "ffmpeg");
87
90
  if (!ffmpegPath) return {};
88
91
  const result = await runner(ffmpegPath, firstFrameColourArgs(filePath), {
89
92
  timeout: 15e3,
90
93
  maxBuffer: 1024 * 1024
91
94
  });
92
- return result.status === 0 ? parseFirstFrameColour(String(result.stderr)) : {};
95
+ const first = result.status === 0 ? parseFirstFrameColour(String(result.stderr)) : {};
96
+ if (result.status !== 0 || !readsKeyframeTransfers(first)) return first;
97
+ const keyframes = await runner(ffmpegPath, keyframeTransferArgs(filePath, codec), {
98
+ timeout: 3e4,
99
+ maxBuffer: 8 * 1024 * 1024
100
+ });
101
+ const keyframeTransfers = keyframes.status === 0 ? parseKeyframeTransfers(String(keyframes.stderr)) : [];
102
+ return { ...first, keyframeTransfers };
93
103
  }
94
104
  function bitDepthOf(stream) {
95
105
  const raw = Number(stream.bits_per_raw_sample);
@@ -35,6 +35,7 @@ interface AudioNormalizationPlan {
35
35
  changeDb: number;
36
36
  projectedLufs: number;
37
37
  projectedTruePeakDbfs: number;
38
+ limitedBy: LoudnessLimit | null;
38
39
  }
39
40
  /** Read the authored clips with sound — `<audio>` and audible `<video>` — without reserializing. */
40
41
  declare function audioTags(html: string): AudioTag[];
@@ -43,7 +44,7 @@ declare function resolveLocalAudioPath(projectDir: string, src: string): string;
43
44
  /** Parse FFmpeg's final EBU R128 summary, not its per-window log lines. */
44
45
  declare function parseEbur128Summary(stderr: string): LoudnessMeasurement;
45
46
  declare function gainDb(volume: number): number;
46
- /** Calculate the target's absolute authored gain while keeping the reference unchanged. */
47
+ /** The target's gain that matches the reference's played loudness; past a ceiling it stops as fittedGain does. */
47
48
  declare function audioNormalizationPlan(referenceTrack: NormalizationTrack, targetTrack: NormalizationTrack): AudioNormalizationPlan;
48
49
  /** Patch one authored attribute in place so scripts/styles/comments remain byte-stable. */
49
50
  declare function updateAudioVolume(html: string, id: string, volume: number): string;
@@ -78,9 +79,8 @@ interface AbsoluteLoudnessPlan {
78
79
  }
79
80
  /**
80
81
  * The authored gain that brings one clip's played window to an absolute
81
- * loudness. Unlike reference matching it never refuses: past the +12 dB
82
- * authoring ceiling or the true-peak ceiling it applies the most that fits and
83
- * names which limit stopped it.
82
+ * loudness; past the +12 dB authoring ceiling or the true-peak ceiling it
83
+ * applies the most that fits and names which limit stopped it (fittedGain).
84
84
  */
85
85
  declare function absoluteLoudnessPlan(track: NormalizationTrack, targetLufs?: number): AbsoluteLoudnessPlan;
86
86
  type AbsoluteOutcome = "matched" | "held" | "change";
@@ -12,8 +12,8 @@ import {
12
12
  requiredFfmpeg,
13
13
  resolveLocalAudioPath,
14
14
  updateAudioVolume
15
- } from "../chunk-H4NVSSDJ.js";
16
- import "../chunk-X73QTST2.js";
15
+ } from "../chunk-NEGQQZNI.js";
16
+ import "../chunk-Y6EZUBYR.js";
17
17
  import "../chunk-QF7MFJJU.js";
18
18
  import "../chunk-SVQFE44C.js";
19
19
  import "../chunk-L3HJVTQE.js";
@@ -15,8 +15,8 @@ import {
15
15
  scanProjectMedia,
16
16
  scanProjectMediaCodecMap,
17
17
  shouldPrewarmProxy
18
- } from "../chunk-NOIHVBIQ.js";
19
- import "../chunk-X73QTST2.js";
18
+ } from "../chunk-66YZA32N.js";
19
+ import "../chunk-Y6EZUBYR.js";
20
20
  import "../chunk-QF7MFJJU.js";
21
21
  import "../chunk-SVQFE44C.js";
22
22
  import "../chunk-L3HJVTQE.js";
@@ -4,10 +4,10 @@ import {
4
4
  isAutoProxyEnabled,
5
5
  proxyEtagSalt,
6
6
  resolvePreviewMediaCodecProbeCache
7
- } from "../chunk-GTQ6S4O5.js";
8
- import "../chunk-B356RYG5.js";
9
- import "../chunk-NOIHVBIQ.js";
10
- import "../chunk-X73QTST2.js";
7
+ } from "../chunk-IJMPYPJG.js";
8
+ import "../chunk-F4AQECCT.js";
9
+ import "../chunk-66YZA32N.js";
10
+ import "../chunk-Y6EZUBYR.js";
11
11
  import "../chunk-QF7MFJJU.js";
12
12
  import "../chunk-SVQFE44C.js";
13
13
  import "../chunk-L3HJVTQE.js";
@@ -22,9 +22,9 @@ import {
22
22
  retryProxyCopy,
23
23
  sourceFolderKind,
24
24
  waitForProxy
25
- } from "../chunk-B356RYG5.js";
26
- import "../chunk-NOIHVBIQ.js";
27
- import "../chunk-X73QTST2.js";
25
+ } from "../chunk-F4AQECCT.js";
26
+ import "../chunk-66YZA32N.js";
27
+ import "../chunk-Y6EZUBYR.js";
28
28
  import "../chunk-QF7MFJJU.js";
29
29
  import "../chunk-SVQFE44C.js";
30
30
  import "../chunk-L3HJVTQE.js";
package/dist/index.js CHANGED
@@ -85,13 +85,13 @@ import {
85
85
  requestedProjectPath,
86
86
  requiredFfmpeg,
87
87
  resolveLocalAudioPath
88
- } from "./chunk-H4NVSSDJ.js";
88
+ } from "./chunk-NEGQQZNI.js";
89
89
  import {
90
90
  injectMediaCodecMap,
91
91
  isAutoProxyEnabled,
92
92
  proxyEtagSalt,
93
93
  resolvePreviewMediaCodecProbeCache
94
- } from "./chunk-GTQ6S4O5.js";
94
+ } from "./chunk-IJMPYPJG.js";
95
95
  import {
96
96
  CACHE_DIR_NAME,
97
97
  PROXY_PENDING_RETRY_AFTER_SECONDS,
@@ -105,7 +105,7 @@ import {
105
105
  resolveStill,
106
106
  retryProxyCopy,
107
107
  waitForProxy
108
- } from "./chunk-B356RYG5.js";
108
+ } from "./chunk-F4AQECCT.js";
109
109
  import {
110
110
  PROXY_VARIANT_CONFIG,
111
111
  decideMediaProxyEligibility,
@@ -113,11 +113,11 @@ import {
113
113
  probeAssetCodec,
114
114
  recordProxyRequest,
115
115
  resolveProxyVariantRequest
116
- } from "./chunk-NOIHVBIQ.js";
116
+ } from "./chunk-66YZA32N.js";
117
117
  import {
118
118
  probeAudioTrack,
119
119
  probeMediaMetadata
120
- } from "./chunk-X73QTST2.js";
120
+ } from "./chunk-Y6EZUBYR.js";
121
121
  import {
122
122
  folderGone,
123
123
  isInHiddenOrVendorDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio-server",
3
- "version": "0.9.16",
3
+ "version": "0.9.17",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/heygen-com/hyperframes",
@@ -88,8 +88,8 @@
88
88
  "linkedom": "^0.18.12",
89
89
  "postcss": "^8.5.8",
90
90
  "postcss-selector-parser": "^7.1.2",
91
- "@hyperframes/core": "0.9.16",
92
- "@hyperframes/parsers": "0.9.16"
91
+ "@hyperframes/parsers": "0.9.17",
92
+ "@hyperframes/core": "0.9.17"
93
93
  },
94
94
  "devDependencies": {
95
95
  "@types/node": "^25.0.10",