@hyperframes/studio-server 0.9.15 → 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";
@@ -343,6 +343,12 @@ function probeElementsInSource(source, targets) {
343
343
  function probeElementInSource(source, target) {
344
344
  return probeElementsInSource(source, [target])[0] ?? false;
345
345
  }
346
+ var refused = (html, refusal) => ({
347
+ html,
348
+ matched: false,
349
+ newId: null,
350
+ refusal
351
+ });
346
352
  function resolveElementTiming(el) {
347
353
  const timing = readClipTiming(el);
348
354
  return { start: timing.start ?? 0, duration: timing.duration ?? 0 };
@@ -363,7 +369,7 @@ function isWindowOf(media, row) {
363
369
  function splitElementInHtml(source, target, splitTime, newId, fallbackTiming) {
364
370
  const { document, wrappedFragment } = parseSourceDocument(source);
365
371
  const el = findTargetElement(document, target);
366
- if (!el || !isHTMLElement(el)) return { html: source, matched: false, newId: null };
372
+ if (!el || !isHTMLElement(el)) return refused(source, "target-not-found");
367
373
  const timedMediaSelector = "video[data-start], audio[data-start]";
368
374
  const innerMedia = el.hasAttribute("data-start") || resolveElementTiming(el).duration > 0 || el.matches("[data-composition-src], [data-composition-id]") ? null : el.querySelector(timedMediaSelector);
369
375
  const timedMedia = innerMedia && isWindowOf(resolveElementTiming(innerMedia), fallbackTiming) ? innerMedia : null;
@@ -373,21 +379,22 @@ function splitElementInHtml(source, target, splitTime, newId, fallbackTiming) {
373
379
  const reference = readClipTiming(timed).startExpression;
374
380
  const startsAfter = reference?.kind === "reference" ? reference : null;
375
381
  if (startsAfter) {
376
- if (!fallbackTiming) return { html: source, matched: false, newId: null };
382
+ if (!fallbackTiming) return refused(source, "reference-without-range");
377
383
  start = fallbackTiming.start;
378
384
  }
379
385
  if (duration <= 0 && fallbackTiming && fallbackTiming.duration > 0) {
380
386
  start = fallbackTiming.start;
381
387
  duration = fallbackTiming.duration;
382
388
  }
383
- if (duration <= 0 || splitTime < start + SPLIT_BOUNDARY_EPSILON_S || splitTime > start + duration - SPLIT_BOUNDARY_EPSILON_S) {
384
- return { html: source, matched: false, newId: null };
389
+ if (duration <= 0) return refused(source, "no-duration");
390
+ if (splitTime < start + SPLIT_BOUNDARY_EPSILON_S || splitTime > start + duration - SPLIT_BOUNDARY_EPSILON_S) {
391
+ return refused(source, "outside-clip");
385
392
  }
386
393
  newId = nextUniqueId(document, newId);
387
394
  const firstDuration = splitTime - start;
388
395
  const secondDuration = duration - firstDuration;
389
396
  const clone = el.cloneNode(true);
390
- if (!isHTMLElement(clone)) return { html: source, matched: false, newId: null };
397
+ if (!isHTMLElement(clone)) return refused(source, "target-not-found");
391
398
  clone.setAttribute("id", newId);
392
399
  dedupeClonedCompositionId(document, clone);
393
400
  clone.removeAttribute("data-hf-id");
@@ -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";
@@ -1,5 +1,5 @@
1
1
  import '@hyperframes/core';
2
- export { a as PreviewApiAdapter, P as PreviewDocumentStore, i as injectMediaCodecMap, f as injectMediaCodecMapIntoHtml, g as isAutoProxyEnabled, p as proxyEtagSalt, r as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-Ds5TElPq.js';
2
+ export { a as PreviewApiAdapter, P as PreviewDocumentStore, i as injectMediaCodecMap, f as injectMediaCodecMapIntoHtml, g as isAutoProxyEnabled, p as proxyEtagSalt, r as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-pNW7nfS0.js';
3
3
  import './mediaCodecMap.js';
4
4
  import 'sharp';
5
5
  import '@hyperframes/parsers';
@@ -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";
@@ -34,10 +34,13 @@ declare function nextUniqueId(document: Document, base: string, takenIds?: Reado
34
34
  /** Whether each target exists in `source`; the document is parsed once however many targets ask. */
35
35
  declare function probeElementsInSource(source: string, targets: readonly SourceMutationTarget[]): boolean[];
36
36
  declare function probeElementInSource(source: string, target: SourceMutationTarget): boolean;
37
+ /** Why a split changed nothing: the cut route says it, so a report names the cause and not just "rejected". */
38
+ type SplitRefusal = "target-not-found" | "reference-without-range" | "no-duration" | "outside-clip";
37
39
  interface SplitElementResult {
38
40
  html: string;
39
41
  matched: boolean;
40
42
  newId: string | null;
43
+ refusal?: SplitRefusal;
41
44
  }
42
45
  declare function splitElementInHtml(source: string, target: SourceMutationTarget, splitTime: number, newId: string, fallbackTiming?: {
43
46
  start: number;
@@ -96,4 +99,4 @@ declare function unwrapElementsFromHtml(source: string, groupTarget: SourceMutat
96
99
  /** After a cut, give each linked group's right halves their own `data-link` and `data-sync-origin`. */
97
100
  declare function relinkSplitHalvesInHtml(source: string, rightHalfIds: readonly string[]): string;
98
101
 
99
- export { type ElementRebase, type PatchOperation, type SourceMutationTarget, type SplitElementResult, type UnwrapChildTrack, type UnwrapElementsResult, type WrapElementsResult, dedupeClonedCompositionId, findTargetElement, isHTMLElement, nextUniqueId, parseSourceDocument, patchElementInHtml, probeElementInSource, probeElementsInSource, relinkSplitHalvesInHtml, removeElementFromHtml, removeElementsFromHtml, splitElementInHtml, unwrapElementsFromHtml, wrapElementsInHtml };
102
+ export { type ElementRebase, type PatchOperation, type SourceMutationTarget, type SplitElementResult, type SplitRefusal, type UnwrapChildTrack, type UnwrapElementsResult, type WrapElementsResult, dedupeClonedCompositionId, findTargetElement, isHTMLElement, nextUniqueId, parseSourceDocument, patchElementInHtml, probeElementInSource, probeElementsInSource, relinkSplitHalvesInHtml, removeElementFromHtml, removeElementsFromHtml, splitElementInHtml, unwrapElementsFromHtml, wrapElementsInHtml };
@@ -13,7 +13,7 @@ import {
13
13
  splitElementInHtml,
14
14
  unwrapElementsFromHtml,
15
15
  wrapElementsInHtml
16
- } from "../chunk-LK3ZTZGN.js";
16
+ } from "../chunk-REQUL5L4.js";
17
17
  export {
18
18
  dedupeClonedCompositionId,
19
19
  findTargetElement,
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Hono } from 'hono';
2
- import { S as StudioApiAdapter, P as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-Ds5TElPq.js';
3
- export { L as LintResult, a as PreviewApiAdapter, R as RenderJobState, b as ResolvedProject, c as StudioSelectionResponse, d as StudioSelectionSnapshot, e as StudioSelectionTextField } from './mediaProxyPreview-Ds5TElPq.js';
2
+ import { S as StudioApiAdapter, P as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-pNW7nfS0.js';
3
+ export { L as LintResult, a as PreviewApiAdapter, R as RenderJobState, b as ResolvedProject, c as StudioSelectionResponse, d as StudioSelectionSnapshot, e as StudioSelectionTextField } from './mediaProxyPreview-pNW7nfS0.js';
4
4
  export { C as ClosedWindow, D as DEFAULT_HISTORY_ROOT, S as HISTORY_START, H as HistoryClosedError, a as HistoryEntry, b as HistoryEntrySide, c as HistoryFileChange, d as HistoryListItem, e as HistoryRefusedError, f as HistoryResult, g as HistoryTarget, h as HistoryWho, i as HistoryWindow, M as MAX_WINDOW_IDLE_MS, P as ProjectHistory, j as ProjectHistoryOptions, k as ProjectHistoryView, U as UNDO_MODES, l as UndoMode, o as openProjectHistory, r as readProjectHistory } from './projectHistory-C18HDvac.js';
5
5
  export { ArtifactCapture, ArtifactInput, ArtifactRecord, HistoryBusyError, HistoryIdError, InPlaceOutcome, ProjectHistoryRecord, PrunedHistory, ReclaimedHistory, RenderRecord, VideoState, artifactDirectory, beginArtifactCapture, finishSnapshotRun, forgetRecordedArtifacts, historyCache, isUncopiedMedia, listProjectHistories, onRestoredInPlace, pruneGoneProjectHistories, reclaimUncopiedBlobs, recordArtifact, recordArtifacts, recordRenderSuccess, registerRenderOutput, retainSnapshotOutput } from './history/index.js';
6
6
  export { CACHE_DIR_NAME as PROXY_CACHE_DIR_NAME } from './helpers/proxyTranscoder.js';
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  splitElementInHtml,
18
18
  unwrapElementsFromHtml,
19
19
  wrapElementsInHtml
20
- } from "./chunk-LK3ZTZGN.js";
20
+ } from "./chunk-REQUL5L4.js";
21
21
  import {
22
22
  isCompositionSource,
23
23
  stampProjectHfIds,
@@ -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,
@@ -2930,6 +2930,12 @@ async function executeGsapMutationRecast(body, block, respond) {
2930
2930
  return respond({ error: `unknown mutation type: ${body.type}` }, 400);
2931
2931
  }
2932
2932
  }
2933
+ var CUT_REFUSALS = {
2934
+ "target-not-found": "Cut target was not found",
2935
+ "reference-without-range": "Cut target starts after another clip and the cut sent no range for it",
2936
+ "no-duration": "Cut target has no duration",
2937
+ "outside-clip": "Cut point was outside the clip's start and end"
2938
+ };
2933
2939
  async function foldAtomicCutFile(c, file, absPath, before, writer) {
2934
2940
  let after = before;
2935
2941
  let splitCount = 0;
@@ -2958,7 +2964,7 @@ async function foldAtomicCutFile(c, file, absPath, before, writer) {
2958
2964
  });
2959
2965
  if (!split.matched || !split.newId) {
2960
2966
  return c.json(
2961
- { error: `Cut target was not found or was outside its authored bounds in ${file.path}` },
2967
+ { error: `${CUT_REFUSALS[split.refusal ?? "target-not-found"]} in ${file.path}` },
2962
2968
  400
2963
2969
  );
2964
2970
  }
@@ -4848,6 +4854,7 @@ function contentDispositionHeader(disposition, filename) {
4848
4854
  }
4849
4855
  function registerRenderRoutes(api, adapter) {
4850
4856
  const renderJobs = /* @__PURE__ */ new Map();
4857
+ const CANCEL_SETTLE_MS = 45e3;
4851
4858
  const nextFreeRenderJobId = (baseId, ext, rendersDir) => {
4852
4859
  let jobId = baseId;
4853
4860
  for (let n = 2; renderJobs.has(jobId) || existsSync6(join10(rendersDir, `${jobId}${ext}`)); n++) {
@@ -4965,13 +4972,19 @@ function registerRenderRoutes(api, adapter) {
4965
4972
  }
4966
4973
  });
4967
4974
  });
4968
- api.post("/render/:jobId/cancel", (c) => {
4975
+ api.post("/render/:jobId/cancel", async (c) => {
4969
4976
  const { jobId } = c.req.param();
4970
4977
  const job = renderJobs.get(jobId);
4971
4978
  if (!job) return c.json({ error: "not found" }, 404);
4972
4979
  if (job.status === "rendering") {
4973
4980
  job.status = "cancelled";
4974
- job.cancel?.();
4981
+ let giveUp;
4982
+ const late = new Promise((done) => giveUp = setTimeout(done, CANCEL_SETTLE_MS));
4983
+ try {
4984
+ await Promise.race([job.cancel?.(), late]);
4985
+ } finally {
4986
+ clearTimeout(giveUp);
4987
+ }
4975
4988
  }
4976
4989
  return c.json({ status: job.status });
4977
4990
  });
@@ -27,7 +27,7 @@ interface RenderJobState {
27
27
  * stop an in-flight render; adapters that can't abort may omit it (the
28
28
  * route still marks the job cancelled so the SSE stream terminates).
29
29
  */
30
- cancel?: () => void;
30
+ cancel?: () => void | Promise<void>;
31
31
  }
32
32
  interface MediaProcessingJobState {
33
33
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio-server",
3
- "version": "0.9.15",
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.15",
92
- "@hyperframes/parsers": "0.9.15"
91
+ "@hyperframes/parsers": "0.9.17",
92
+ "@hyperframes/core": "0.9.17"
93
93
  },
94
94
  "devDependencies": {
95
95
  "@types/node": "^25.0.10",