hyperframes 0.5.0-alpha.5 → 0.5.0-alpha.7

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/dist/cli.js CHANGED
@@ -54,7 +54,7 @@ var VERSION;
54
54
  var init_version = __esm({
55
55
  "src/version.ts"() {
56
56
  "use strict";
57
- VERSION = true ? "0.5.0-alpha.5" : "0.0.0-dev";
57
+ VERSION = true ? "0.5.0-alpha.7" : "0.0.0-dev";
58
58
  }
59
59
  });
60
60
 
@@ -5438,7 +5438,7 @@ var init_captions = __esm({
5438
5438
  const hasHardKill = /\.set\s*\([^,]+,\s*\{[^}]*(?:visibility\s*:\s*["']hidden["']|opacity\s*:\s*0)/.test(
5439
5439
  content
5440
5440
  );
5441
- const hasCaptionLoop = /forEach|\.forEach\s*\(/.test(content) && /createElement|caption|group|cg-/.test(content);
5441
+ const hasCaptionLoop = /forEach|\.forEach\s*\(/.test(content) && /karaoke|caption[-_]?(?:group|word|line|block)|cg-/.test(content);
5442
5442
  if (hasCaptionLoop && hasExitTween && !hasHardKill) {
5443
5443
  findings.push({
5444
5444
  code: "caption_exit_missing_hard_kill",
@@ -9490,9 +9490,12 @@ function readCache(path2) {
9490
9490
  }
9491
9491
  }
9492
9492
  function writeCache(path2, data) {
9493
- mkdirSync(dirname3(path2), { recursive: true });
9494
- const entry = { fetchedAt: Date.now(), data };
9495
- writeFileSync(path2, JSON.stringify(entry), "utf-8");
9493
+ try {
9494
+ mkdirSync(dirname3(path2), { recursive: true });
9495
+ const entry = { fetchedAt: Date.now(), data };
9496
+ writeFileSync(path2, JSON.stringify(entry), "utf-8");
9497
+ } catch {
9498
+ }
9496
9499
  }
9497
9500
  async function fetchJson(url) {
9498
9501
  const res = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
@@ -25024,6 +25027,8 @@ function registerThumbnailRoutes(api, adapter2) {
25024
25027
  const vpWidth = parseInt(url.searchParams.get("w") || "0") || 0;
25025
25028
  const vpHeight = parseInt(url.searchParams.get("h") || "0") || 0;
25026
25029
  const selector = url.searchParams.get("selector") || void 0;
25030
+ const rawSelectorIndex = Number.parseInt(url.searchParams.get("selectorIndex") || "0", 10);
25031
+ const selectorIndex = Number.isFinite(rawSelectorIndex) && rawSelectorIndex > 0 ? rawSelectorIndex : void 0;
25027
25032
  const urlVersion = url.searchParams.get("v") || "";
25028
25033
  let compW = vpWidth || 1920;
25029
25034
  let compH = vpHeight || 1080;
@@ -25041,7 +25046,7 @@ function registerThumbnailRoutes(api, adapter2) {
25041
25046
  }
25042
25047
  const previewUrl = compPath === "index.html" ? `http://${c2.req.header("host")}/api/projects/${project.id}/preview` : `http://${c2.req.header("host")}/api/projects/${project.id}/preview/comp/${compPath}`;
25043
25048
  const cacheDir = join17(project.dir, ".thumbnails");
25044
- const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}` : "";
25049
+ const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${selectorIndex ?? 0}` : "";
25045
25050
  const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
25046
25051
  const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}_${compPath.replace(/\//g, "_")}_${compW}x${compH}_${sourceMtime}_${seekTime.toFixed(2)}${selectorKey}.jpg`;
25047
25052
  const cachePath2 = join17(cacheDir, cacheKey);
@@ -25058,7 +25063,8 @@ function registerThumbnailRoutes(api, adapter2) {
25058
25063
  width: compW,
25059
25064
  height: compH,
25060
25065
  previewUrl,
25061
- selector
25066
+ selector,
25067
+ selectorIndex
25062
25068
  });
25063
25069
  if (!buffer) {
25064
25070
  return c2.json({ error: "Thumbnail generation returned null" }, 500);
@@ -29123,6 +29129,7 @@ function calculateOptimalWorkers(totalFrames, requested, config) {
29123
29129
  const effectiveCoresPerWorker = config?.coresPerWorker ?? DEFAULT_CONFIG2.coresPerWorker;
29124
29130
  const effectiveMinParallelFrames = config?.minParallelFrames ?? DEFAULT_CONFIG2.minParallelFrames;
29125
29131
  const effectiveLargeRenderThreshold = config?.largeRenderThreshold ?? DEFAULT_CONFIG2.largeRenderThreshold;
29132
+ const captureCostMultiplier = Math.max(1, config?.captureCostMultiplier ?? 1);
29126
29133
  if (requested !== void 0) {
29127
29134
  return Math.max(MIN_WORKERS, Math.min(effectiveMaxWorkers, requested));
29128
29135
  }
@@ -29135,8 +29142,14 @@ function calculateOptimalWorkers(totalFrames, requested, config) {
29135
29142
  const optimal = Math.min(cpuBasedWorkers, memoryBasedWorkers, frameBasedWorkers);
29136
29143
  const minWorkersForJob = totalFrames >= effectiveMinParallelFrames ? 2 : MIN_WORKERS;
29137
29144
  let finalWorkers = Math.max(minWorkersForJob, Math.min(effectiveMaxWorkers, optimal));
29138
- if (totalFrames >= effectiveLargeRenderThreshold) {
29139
- const cpuScaledMax = Math.max(2, Math.floor(cpuCount / effectiveCoresPerWorker));
29145
+ const weightedFrames = totalFrames * captureCostMultiplier;
29146
+ const contentionThreshold = Math.max(
29147
+ effectiveMinParallelFrames,
29148
+ Math.floor(effectiveLargeRenderThreshold / 3)
29149
+ );
29150
+ if (totalFrames >= effectiveLargeRenderThreshold || weightedFrames >= contentionThreshold) {
29151
+ const weightedCoresPerWorker = effectiveCoresPerWorker * captureCostMultiplier;
29152
+ const cpuScaledMax = Math.max(MIN_WORKERS, Math.floor(cpuCount / weightedCoresPerWorker));
29140
29153
  if (finalWorkers > cpuScaledMax) {
29141
29154
  finalWorkers = cpuScaledMax;
29142
29155
  }
@@ -32896,6 +32909,17 @@ function detectRenderModeHints(html) {
32896
32909
  reasons
32897
32910
  };
32898
32911
  }
32912
+ function detectShaderTransitionUsage(html) {
32913
+ let scriptMatch;
32914
+ const scriptPattern = new RegExp(INLINE_SCRIPT_PATTERN.source, INLINE_SCRIPT_PATTERN.flags);
32915
+ while ((scriptMatch = scriptPattern.exec(html)) !== null) {
32916
+ const attrs = scriptMatch[1] || "";
32917
+ if (/\bsrc\s*=/i.test(attrs)) continue;
32918
+ const content = stripJsComments(stripCompilerMountBootstrap(scriptMatch[2] || ""));
32919
+ if (SHADER_TRANSITION_USAGE_PATTERN.test(content)) return true;
32920
+ }
32921
+ return false;
32922
+ }
32899
32923
  async function resolveMediaDuration(src, mediaStart, baseDir, downloadDir, tagName19) {
32900
32924
  let filePath = src;
32901
32925
  if (isHttpUrl(src)) {
@@ -33432,6 +33456,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
33432
33456
  "$1"
33433
33457
  );
33434
33458
  const renderModeHints = detectRenderModeHints(sanitizedHtml);
33459
+ const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
33435
33460
  const coalescedHtml = await injectDeterministicFontFaces(
33436
33461
  coalesceHeadStylesAndBodyScripts2(promoteCssImportsToLinkTags(sanitizedHtml))
33437
33462
  );
@@ -33479,7 +33504,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
33479
33504
  width,
33480
33505
  height,
33481
33506
  staticDuration,
33482
- renderModeHints
33507
+ renderModeHints,
33508
+ hasShaderTransitions
33483
33509
  };
33484
33510
  }
33485
33511
  async function discoverMediaFromBrowser(page) {
@@ -33581,10 +33607,11 @@ async function recompileWithResolutions(compiled, resolutions, projectDir, downl
33581
33607
  audios,
33582
33608
  images,
33583
33609
  unresolvedCompositions: remaining,
33584
- renderModeHints: compiled.renderModeHints
33610
+ renderModeHints: compiled.renderModeHints,
33611
+ hasShaderTransitions: compiled.hasShaderTransitions
33585
33612
  };
33586
33613
  }
33587
- var INLINE_SCRIPT_PATTERN, COMPILER_MOUNT_BLOCK_START, COMPILER_MOUNT_BLOCK_END;
33614
+ var INLINE_SCRIPT_PATTERN, COMPILER_MOUNT_BLOCK_START, COMPILER_MOUNT_BLOCK_END, SHADER_TRANSITION_USAGE_PATTERN;
33588
33615
  var init_htmlCompiler2 = __esm({
33589
33616
  "../producer/src/services/htmlCompiler.ts"() {
33590
33617
  "use strict";
@@ -33599,6 +33626,7 @@ var init_htmlCompiler2 = __esm({
33599
33626
  INLINE_SCRIPT_PATTERN = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
33600
33627
  COMPILER_MOUNT_BLOCK_START = "/* __HF_COMPILER_MOUNT_START__ */";
33601
33628
  COMPILER_MOUNT_BLOCK_END = "/* __HF_COMPILER_MOUNT_END__ */";
33629
+ SHADER_TRANSITION_USAGE_PATTERN = /\b(?:(?:window|globalThis)\s*\.\s*)?HyperShader\s*\.\s*init\s*\(|\b__hf\s*\.\s*transitions\s*=/;
33602
33630
  }
33603
33631
  });
33604
33632
 
@@ -33902,7 +33930,8 @@ function writeCompiledArtifacts(compiled, workDir, includeSummary) {
33902
33930
  mediaStart: a.mediaStart
33903
33931
  })),
33904
33932
  subCompositions: Array.from(compiled.subCompositions.keys()),
33905
- renderModeHints: compiled.renderModeHints
33933
+ renderModeHints: compiled.renderModeHints,
33934
+ hasShaderTransitions: compiled.hasShaderTransitions
33906
33935
  };
33907
33936
  writeFileSync14(join36(compileDir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
33908
33937
  }
@@ -33915,6 +33944,287 @@ function applyRenderModeHints(cfg, compiled, log2 = defaultLogger) {
33915
33944
  reasons: compiled.renderModeHints.reasons.map((reason) => reason.message)
33916
33945
  });
33917
33946
  }
33947
+ function resolveRenderWorkerCount(totalFrames, requestedWorkers, cfg, compiled, composition, log2 = defaultLogger, measuredCaptureCost) {
33948
+ const captureCost = combineCaptureCostEstimates(
33949
+ estimateCaptureCostMultiplier(compiled, composition),
33950
+ measuredCaptureCost
33951
+ );
33952
+ const workerCount = calculateOptimalWorkers(totalFrames, requestedWorkers, {
33953
+ ...cfg,
33954
+ captureCostMultiplier: captureCost.multiplier
33955
+ });
33956
+ if (requestedWorkers !== void 0 || captureCost.multiplier <= 1) {
33957
+ return workerCount;
33958
+ }
33959
+ const baselineWorkers = calculateOptimalWorkers(totalFrames, void 0, cfg);
33960
+ if (workerCount < baselineWorkers) {
33961
+ log2.warn(
33962
+ "[Render] Reduced auto worker count for high-cost capture workload to avoid Chrome compositor starvation.",
33963
+ {
33964
+ from: baselineWorkers,
33965
+ to: workerCount,
33966
+ costMultiplier: captureCost.multiplier,
33967
+ reasons: captureCost.reasons
33968
+ }
33969
+ );
33970
+ }
33971
+ return workerCount;
33972
+ }
33973
+ function estimateCaptureCostMultiplier(compiled, composition) {
33974
+ let multiplier = 1;
33975
+ const reasons = [];
33976
+ if (compiled.hasShaderTransitions) {
33977
+ multiplier += 2;
33978
+ reasons.push("shader-transitions");
33979
+ }
33980
+ const reasonCodes = new Set(compiled.renderModeHints.reasons.map((reason) => reason.code));
33981
+ if (reasonCodes.has("requestAnimationFrame")) {
33982
+ multiplier += 1;
33983
+ reasons.push("requestAnimationFrame");
33984
+ }
33985
+ if (reasonCodes.has("iframe")) {
33986
+ multiplier += 0.5;
33987
+ reasons.push("iframe");
33988
+ }
33989
+ if (composition.videos.length > 0) {
33990
+ multiplier += Math.min(2, composition.videos.length * 0.75);
33991
+ reasons.push(`${composition.videos.length} video${composition.videos.length === 1 ? "" : "s"}`);
33992
+ }
33993
+ if (composition.audios.length > 0) {
33994
+ multiplier += Math.min(1, composition.audios.length * 0.75);
33995
+ reasons.push(`${composition.audios.length} audio${composition.audios.length === 1 ? "" : "s"}`);
33996
+ }
33997
+ return {
33998
+ multiplier: Math.round(multiplier * 100) / 100,
33999
+ reasons
34000
+ };
34001
+ }
34002
+ function combineCaptureCostEstimates(staticCost, measuredCost) {
34003
+ if (!measuredCost || measuredCost.multiplier <= 1) return staticCost;
34004
+ if (staticCost.multiplier >= measuredCost.multiplier) {
34005
+ return {
34006
+ multiplier: staticCost.multiplier,
34007
+ reasons: [...staticCost.reasons, ...measuredCost.reasons],
34008
+ p95Ms: measuredCost.p95Ms
34009
+ };
34010
+ }
34011
+ return {
34012
+ multiplier: measuredCost.multiplier,
34013
+ reasons: [...measuredCost.reasons, ...staticCost.reasons],
34014
+ p95Ms: measuredCost.p95Ms
34015
+ };
34016
+ }
34017
+ function createCaptureCalibrationConfig(cfg) {
34018
+ return {
34019
+ ...cfg,
34020
+ protocolTimeout: Math.min(cfg.protocolTimeout, CAPTURE_CALIBRATION_PROTOCOL_TIMEOUT_MS)
34021
+ };
34022
+ }
34023
+ function estimateMeasuredCaptureCostMultiplier(samples) {
34024
+ if (samples.length === 0) {
34025
+ return { multiplier: 1, reasons: [] };
34026
+ }
34027
+ const sorted = [...samples].sort((a, b) => a.captureTimeMs - b.captureTimeMs);
34028
+ const p95Index = Math.max(0, Math.ceil(sorted.length * 0.95) - 1);
34029
+ const p95Sample = sorted[p95Index] ?? sorted[sorted.length - 1];
34030
+ if (!p95Sample) {
34031
+ return { multiplier: 1, reasons: [] };
34032
+ }
34033
+ const p95Ms = Math.round(p95Sample.captureTimeMs);
34034
+ const multiplier = Math.min(
34035
+ MAX_MEASURED_CAPTURE_COST_MULTIPLIER,
34036
+ Math.max(1, Math.round(p95Ms / CAPTURE_CALIBRATION_TARGET_MS * 100) / 100)
34037
+ );
34038
+ return {
34039
+ multiplier,
34040
+ reasons: multiplier > 1 ? [`calibration-p95=${p95Ms}ms`] : [],
34041
+ p95Ms
34042
+ };
34043
+ }
34044
+ function selectCaptureCalibrationFrames(totalFrames) {
34045
+ if (totalFrames <= 0) return [];
34046
+ const lastFrame = totalFrames - 1;
34047
+ const candidates = [
34048
+ 0,
34049
+ Math.floor(totalFrames * 0.25),
34050
+ Math.floor(totalFrames * 0.5),
34051
+ Math.floor(totalFrames * 0.75),
34052
+ lastFrame
34053
+ ];
34054
+ return Array.from(
34055
+ new Set(candidates.map((frame) => Math.max(0, Math.min(lastFrame, frame))))
34056
+ ).sort((a, b) => a - b);
34057
+ }
34058
+ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
34059
+ const ranges = [];
34060
+ let rangeStart = null;
34061
+ for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
34062
+ const framePath = join36(framesDir, `frame_${String(frameIndex).padStart(6, "0")}.${frameExt}`);
34063
+ const missing = !existsSync34(framePath);
34064
+ if (missing && rangeStart === null) {
34065
+ rangeStart = frameIndex;
34066
+ } else if (!missing && rangeStart !== null) {
34067
+ ranges.push({ startFrame: rangeStart, endFrame: frameIndex });
34068
+ rangeStart = null;
34069
+ }
34070
+ }
34071
+ if (rangeStart !== null) {
34072
+ ranges.push({ startFrame: rangeStart, endFrame: totalFrames });
34073
+ }
34074
+ return ranges;
34075
+ }
34076
+ function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt) {
34077
+ const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
34078
+ const batches = [];
34079
+ for (let i2 = 0; i2 < ranges.length; i2 += workersPerBatch) {
34080
+ const batchIndex = batches.length;
34081
+ const batch = ranges.slice(i2, i2 + workersPerBatch).map((range, workerId) => ({
34082
+ workerId,
34083
+ startFrame: range.startFrame,
34084
+ endFrame: range.endFrame,
34085
+ outputDir: join36(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`)
34086
+ }));
34087
+ batches.push(batch);
34088
+ }
34089
+ return batches;
34090
+ }
34091
+ function getNextRetryWorkerCount(currentWorkers) {
34092
+ return Math.max(1, Math.floor(currentWorkers / 2));
34093
+ }
34094
+ function isRecoverableParallelCaptureError(error) {
34095
+ const message = error instanceof Error ? error.message : String(error);
34096
+ return message.includes("[Parallel] Capture failed") && /Runtime\.callFunctionOn timed out|HeadlessExperimental\.beginFrame timed out|Waiting failed|timeout exceeded|timed out|Navigation timeout|Protocol error|Target closed/i.test(
34097
+ message
34098
+ );
34099
+ }
34100
+ function shouldFallbackToScreenshotAfterCalibrationError(error) {
34101
+ const message = error instanceof Error ? error.message : String(error);
34102
+ return /HeadlessExperimental\.beginFrame timed out|beginFrame probe timeout|Another frame is pending|Frame still pending|Protocol error.*HeadlessExperimental\.beginFrame/i.test(
34103
+ message
34104
+ );
34105
+ }
34106
+ function countCapturedFrames(totalFrames, framesDir, frameExt) {
34107
+ let captured = 0;
34108
+ for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
34109
+ const framePath = join36(framesDir, `frame_${String(frameIndex).padStart(6, "0")}.${frameExt}`);
34110
+ if (existsSync34(framePath)) captured++;
34111
+ }
34112
+ return captured;
34113
+ }
34114
+ function countFrameRanges(ranges) {
34115
+ return ranges.reduce((sum, range) => sum + (range.endFrame - range.startFrame), 0);
34116
+ }
34117
+ async function measureCaptureCostFromSession(session, totalFrames, fps) {
34118
+ const sampledFrames = selectCaptureCalibrationFrames(totalFrames);
34119
+ const samples = [];
34120
+ for (const frameIndex of sampledFrames) {
34121
+ const time = frameIndex / fps;
34122
+ const startedAt = Date.now();
34123
+ const result = await captureFrameToBuffer(session, frameIndex, time);
34124
+ samples.push({
34125
+ frameIndex,
34126
+ captureTimeMs: result.captureTimeMs || Date.now() - startedAt
34127
+ });
34128
+ }
34129
+ return {
34130
+ estimate: estimateMeasuredCaptureCostMultiplier(samples),
34131
+ samples
34132
+ };
34133
+ }
34134
+ async function executeDiskCaptureWithAdaptiveRetry(options) {
34135
+ const attempts = [];
34136
+ let currentWorkers = options.initialWorkerCount;
34137
+ let missingRanges = null;
34138
+ let attempt = 0;
34139
+ while (true) {
34140
+ const frameCount = missingRanges ? countFrameRanges(missingRanges) : options.totalFrames;
34141
+ attempts.push({
34142
+ attempt,
34143
+ workers: currentWorkers,
34144
+ frameCount,
34145
+ reason: attempt === 0 ? "initial" : "retry"
34146
+ });
34147
+ const attemptWorkDir = join36(options.workDir, `capture-attempt-${attempt}`);
34148
+ const batches = missingRanges ? buildMissingFrameRetryBatches(missingRanges, currentWorkers, attemptWorkDir, attempt) : [distributeFrames(options.totalFrames, currentWorkers, attemptWorkDir)];
34149
+ try {
34150
+ for (const tasks of batches) {
34151
+ const capturedBeforeBatch = countCapturedFrames(
34152
+ options.totalFrames,
34153
+ options.framesDir,
34154
+ options.frameExt
34155
+ );
34156
+ try {
34157
+ await executeParallelCapture(
34158
+ options.serverUrl,
34159
+ attemptWorkDir,
34160
+ tasks,
34161
+ options.captureOptions,
34162
+ options.createBeforeCaptureHook,
34163
+ options.abortSignal,
34164
+ options.onProgress ? (progress) => {
34165
+ options.onProgress?.({
34166
+ ...progress,
34167
+ totalFrames: options.totalFrames,
34168
+ capturedFrames: Math.min(
34169
+ options.totalFrames,
34170
+ capturedBeforeBatch + progress.capturedFrames
34171
+ )
34172
+ });
34173
+ } : void 0,
34174
+ void 0,
34175
+ options.cfg
34176
+ );
34177
+ } finally {
34178
+ await mergeWorkerFrames(attemptWorkDir, tasks, options.framesDir);
34179
+ }
34180
+ }
34181
+ const remaining = findMissingFrameRanges(
34182
+ options.totalFrames,
34183
+ options.framesDir,
34184
+ options.frameExt
34185
+ );
34186
+ if (remaining.length === 0) {
34187
+ return attempts;
34188
+ }
34189
+ if (!options.allowRetry || currentWorkers <= 1) {
34190
+ throw new Error(
34191
+ `[Render] Capture completed but ${countFrameRanges(remaining)} frame(s) are missing`
34192
+ );
34193
+ }
34194
+ const nextWorkers = getNextRetryWorkerCount(currentWorkers);
34195
+ options.log.warn("[Render] Retrying missing captured frames with fewer workers.", {
34196
+ fromWorkers: currentWorkers,
34197
+ toWorkers: nextWorkers,
34198
+ missingFrames: countFrameRanges(remaining)
34199
+ });
34200
+ currentWorkers = nextWorkers;
34201
+ missingRanges = remaining;
34202
+ attempt++;
34203
+ } catch (error) {
34204
+ const remaining = findMissingFrameRanges(
34205
+ options.totalFrames,
34206
+ options.framesDir,
34207
+ options.frameExt
34208
+ );
34209
+ if (remaining.length === 0) {
34210
+ return attempts;
34211
+ }
34212
+ if (!options.allowRetry || currentWorkers <= 1 || !isRecoverableParallelCaptureError(error)) {
34213
+ throw error;
34214
+ }
34215
+ const nextWorkers = getNextRetryWorkerCount(currentWorkers);
34216
+ options.log.warn("[Render] Parallel capture timed out; retrying missing frames.", {
34217
+ fromWorkers: currentWorkers,
34218
+ toWorkers: nextWorkers,
34219
+ missingFrames: countFrameRanges(remaining),
34220
+ error: error instanceof Error ? error.message : String(error)
34221
+ });
34222
+ currentWorkers = nextWorkers;
34223
+ missingRanges = remaining;
34224
+ attempt++;
34225
+ }
34226
+ }
34227
+ }
33918
34228
  function blitHdrVideoLayer(canvas, el, time, fps, hdrFrameDirs, hdrStartTimes, width, height, log2, sourceTransfer, targetTransfer) {
33919
34229
  const frameDir = hdrFrameDirs.get(el.id);
33920
34230
  const startTime = hdrStartTimes.get(el.id);
@@ -34541,7 +34851,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
34541
34851
  let extractionResult = null;
34542
34852
  const nativeHdrVideoIds = /* @__PURE__ */ new Set();
34543
34853
  const videoTransfers = /* @__PURE__ */ new Map();
34544
- if (job.config.hdr && composition.videos.length > 0) {
34854
+ if (job.config.hdrMode !== "force-sdr" && composition.videos.length > 0) {
34545
34855
  await Promise.all(
34546
34856
  composition.videos.map(async (v) => {
34547
34857
  let videoPath = v.src;
@@ -34562,7 +34872,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
34562
34872
  const imageTransfers = /* @__PURE__ */ new Map();
34563
34873
  const hdrImageSrcPaths = /* @__PURE__ */ new Map();
34564
34874
  const imageColorSpaces = [];
34565
- if (job.config.hdr && composition.images.length > 0) {
34875
+ if (job.config.hdrMode !== "force-sdr" && composition.images.length > 0) {
34566
34876
  const probed = await Promise.all(
34567
34877
  composition.images.map(async (img) => {
34568
34878
  let imgPath = img.src;
@@ -34619,28 +34929,53 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
34619
34929
  perfStages.videoExtractMs = Date.now() - stage2Start;
34620
34930
  }
34621
34931
  let effectiveHdr;
34622
- if (job.config.hdr) {
34932
+ let forcedHdrWithoutSources = false;
34933
+ {
34934
+ const hdrMode = job.config.hdrMode ?? "auto";
34623
34935
  const videoColorSpaces = (extractionResult?.extracted ?? []).map(
34624
34936
  (ext) => ext.metadata.colorSpace
34625
34937
  );
34626
34938
  const allColorSpaces = [...videoColorSpaces, ...imageColorSpaces];
34627
- if (allColorSpaces.length > 0) {
34628
- const info = analyzeCompositionHdr(allColorSpaces);
34629
- if (info.hasHdr && info.dominantTransfer) {
34939
+ const info = allColorSpaces.length > 0 ? analyzeCompositionHdr(allColorSpaces) : null;
34940
+ if (hdrMode === "force-sdr") {
34941
+ effectiveHdr = void 0;
34942
+ } else if (hdrMode === "force-hdr") {
34943
+ if (info?.hasHdr && info.dominantTransfer) {
34944
+ effectiveHdr = { transfer: info.dominantTransfer };
34945
+ } else {
34946
+ effectiveHdr = { transfer: "hlg" };
34947
+ forcedHdrWithoutSources = true;
34948
+ }
34949
+ } else {
34950
+ if (info?.hasHdr && info.dominantTransfer) {
34630
34951
  effectiveHdr = { transfer: info.dominantTransfer };
34631
34952
  }
34632
34953
  }
34633
34954
  }
34634
34955
  if (effectiveHdr && outputFormat !== "mp4") {
34956
+ const hdrSourceReason = forcedHdrWithoutSources ? "HDR was forced without detected HDR sources" : "HDR source detected";
34635
34957
  log2.warn(
34636
- `[Render] HDR source detected but format is "${outputFormat}" \u2014 falling back to SDR. HDR + alpha is not supported. Use --format mp4 for HDR10 output.`
34958
+ `[Render] ${hdrSourceReason}, but format is "${outputFormat}" \u2014 falling back to SDR. HDR + alpha is not supported. Use --format mp4 for HDR10 output.`
34637
34959
  );
34638
34960
  effectiveHdr = void 0;
34639
34961
  }
34640
- if (effectiveHdr) {
34641
- log2.info(
34642
- `[Render] HDR source detected \u2014 output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`
34643
- );
34962
+ {
34963
+ const hdrMode = job.config.hdrMode ?? "auto";
34964
+ if (forcedHdrWithoutSources) {
34965
+ log2.warn(
34966
+ "[Render] HDR forced by --hdr flag, but no HDR sources were detected \u2014 defaulting to HLG. SDR-only compositions may look perceptually wrong on HDR displays."
34967
+ );
34968
+ }
34969
+ if (effectiveHdr) {
34970
+ const reason = hdrMode === "force-hdr" ? forcedHdrWithoutSources ? "forced by --hdr flag (no HDR sources detected \u2014 defaulting to HLG)" : "forced by --hdr flag" : "auto-detected from source(s)";
34971
+ log2.info(
34972
+ `[Render] HDR ${reason} \u2014 output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`
34973
+ );
34974
+ } else if (hdrMode === "force-sdr") {
34975
+ log2.info("[Render] SDR forced by --sdr flag");
34976
+ } else {
34977
+ log2.info("[Render] No HDR sources detected \u2014 rendering SDR");
34978
+ }
34644
34979
  }
34645
34980
  const stage3Start = Date.now();
34646
34981
  updateJobStatus(job, "preprocessing", "Processing audio tracks", 20, onProgress);
@@ -34687,7 +35022,101 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
34687
35022
  ...captureOptions,
34688
35023
  skipReadinessVideoIds: Array.from(nativeHdrVideoIds)
34689
35024
  });
34690
- const workerCount = calculateOptimalWorkers(totalFrames, job.config.workers, cfg);
35025
+ let captureCalibration;
35026
+ let switchedToScreenshotAfterCalibration = false;
35027
+ if (job.config.workers === void 0 && totalFrames >= 60) {
35028
+ const calibrationDir = join36(workDir, "capture-calibration");
35029
+ const calibrationCfg = createCaptureCalibrationConfig(cfg);
35030
+ const videoInjector = createVideoFrameInjector(frameLookup);
35031
+ let calibrationSession = null;
35032
+ try {
35033
+ calibrationSession = await createCaptureSession(
35034
+ fileServer.url,
35035
+ calibrationDir,
35036
+ buildHdrCaptureOptions(),
35037
+ videoInjector,
35038
+ calibrationCfg
35039
+ );
35040
+ if (!calibrationSession.isInitialized) {
35041
+ await initializeSession(calibrationSession);
35042
+ }
35043
+ assertNotAborted();
35044
+ captureCalibration = await measureCaptureCostFromSession(
35045
+ calibrationSession,
35046
+ totalFrames,
35047
+ job.config.fps
35048
+ );
35049
+ if (captureCalibration.estimate.multiplier > 1) {
35050
+ log2.warn("[Render] Measured slow frame capture during auto-worker calibration.", {
35051
+ multiplier: captureCalibration.estimate.multiplier,
35052
+ p95Ms: captureCalibration.estimate.p95Ms,
35053
+ sampledFrames: captureCalibration.samples.map((sample) => sample.frameIndex)
35054
+ });
35055
+ } else {
35056
+ log2.debug("[Render] Auto-worker calibration kept baseline capture cost.", {
35057
+ p95Ms: captureCalibration.estimate.p95Ms,
35058
+ sampledFrames: captureCalibration.samples.map((sample) => sample.frameIndex)
35059
+ });
35060
+ }
35061
+ } catch (error) {
35062
+ const shouldFallbackToScreenshot = !cfg.forceScreenshot && shouldFallbackToScreenshotAfterCalibrationError(error);
35063
+ if (shouldFallbackToScreenshot) {
35064
+ cfg.forceScreenshot = true;
35065
+ switchedToScreenshotAfterCalibration = true;
35066
+ if (probeSession) {
35067
+ lastBrowserConsole = probeSession.browserConsoleBuffer;
35068
+ await closeCaptureSession(probeSession).catch(() => {
35069
+ });
35070
+ probeSession = null;
35071
+ }
35072
+ }
35073
+ captureCalibration = {
35074
+ estimate: {
35075
+ multiplier: MAX_MEASURED_CAPTURE_COST_MULTIPLIER,
35076
+ reasons: shouldFallbackToScreenshot ? ["calibration-beginframe-timeout", "screenshot-fallback"] : ["calibration-failed"]
35077
+ },
35078
+ samples: []
35079
+ };
35080
+ if (shouldFallbackToScreenshot) {
35081
+ log2.warn(
35082
+ "[Render] BeginFrame auto-worker calibration timed out; falling back to screenshot capture mode.",
35083
+ {
35084
+ protocolTimeout: calibrationCfg.protocolTimeout,
35085
+ error: error instanceof Error ? error.message : String(error)
35086
+ }
35087
+ );
35088
+ } else {
35089
+ log2.warn("[Render] Auto-worker calibration failed; using conservative worker budget.", {
35090
+ protocolTimeout: calibrationCfg.protocolTimeout,
35091
+ error: error instanceof Error ? error.message : String(error)
35092
+ });
35093
+ }
35094
+ } finally {
35095
+ if (calibrationSession) {
35096
+ lastBrowserConsole = calibrationSession.browserConsoleBuffer;
35097
+ await closeCaptureSession(calibrationSession).catch(() => {
35098
+ });
35099
+ }
35100
+ }
35101
+ }
35102
+ let workerCount = resolveRenderWorkerCount(
35103
+ totalFrames,
35104
+ job.config.workers,
35105
+ cfg,
35106
+ compiled,
35107
+ composition,
35108
+ log2,
35109
+ captureCalibration?.estimate
35110
+ );
35111
+ if (switchedToScreenshotAfterCalibration && workerCount > 1) {
35112
+ workerCount = 1;
35113
+ }
35114
+ if (workerCount > 1 && probeSession) {
35115
+ lastBrowserConsole = probeSession.browserConsoleBuffer;
35116
+ await closeCaptureSession(probeSession);
35117
+ probeSession = null;
35118
+ }
35119
+ const captureAttempts = [];
34691
35120
  const FORMAT_EXT2 = {
34692
35121
  mp4: ".mp4",
34693
35122
  webm: ".webm",
@@ -35279,15 +35708,18 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35279
35708
  perfStages.encodeMs = encodeResult.durationMs;
35280
35709
  } else {
35281
35710
  if (workerCount > 1) {
35282
- const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
35283
- await executeParallelCapture(
35284
- fileServer.url,
35711
+ const attempts = await executeDiskCaptureWithAdaptiveRetry({
35712
+ serverUrl: fileServer.url,
35285
35713
  workDir,
35286
- tasks,
35287
- buildHdrCaptureOptions(),
35288
- () => createVideoFrameInjector(frameLookup),
35714
+ framesDir,
35715
+ totalFrames: job.totalFrames,
35716
+ initialWorkerCount: workerCount,
35717
+ allowRetry: job.config.workers === void 0,
35718
+ frameExt: needsAlpha ? "png" : "jpg",
35719
+ captureOptions: buildHdrCaptureOptions(),
35720
+ createBeforeCaptureHook: () => createVideoFrameInjector(frameLookup),
35289
35721
  abortSignal,
35290
- (progress) => {
35722
+ onProgress: (progress) => {
35291
35723
  job.framesRendered = progress.capturedFrames;
35292
35724
  const frameProgress = progress.capturedFrames / progress.totalFrames;
35293
35725
  const progressPct = 25 + frameProgress * 45;
@@ -35295,16 +35727,20 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35295
35727
  updateJobStatus(
35296
35728
  job,
35297
35729
  "rendering",
35298
- `Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
35730
+ `Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${progress.activeWorkers} workers)`,
35299
35731
  Math.round(progressPct),
35300
35732
  onProgress
35301
35733
  );
35302
35734
  }
35303
35735
  },
35304
- void 0,
35305
- cfg
35306
- );
35307
- await mergeWorkerFrames(workDir, tasks, framesDir);
35736
+ cfg,
35737
+ log: log2
35738
+ });
35739
+ captureAttempts.push(...attempts);
35740
+ const lastAttempt = attempts[attempts.length - 1];
35741
+ if (lastAttempt) {
35742
+ workerCount = lastAttempt.workers;
35743
+ }
35308
35744
  if (probeSession) {
35309
35745
  lastBrowserConsole = probeSession.browserConsoleBuffer;
35310
35746
  await closeCaptureSession(probeSession);
@@ -35474,6 +35910,13 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35474
35910
  stages: perfStages,
35475
35911
  videoExtractBreakdown: extractionResult?.phaseBreakdown,
35476
35912
  tmpPeakBytes,
35913
+ captureCalibration: captureCalibration ? {
35914
+ sampledFrames: captureCalibration.samples.map((sample) => sample.frameIndex),
35915
+ p95Ms: captureCalibration.estimate.p95Ms,
35916
+ multiplier: captureCalibration.estimate.multiplier,
35917
+ reasons: captureCalibration.estimate.reasons
35918
+ } : void 0,
35919
+ captureAttempts: captureAttempts.length > 0 ? captureAttempts : void 0,
35477
35920
  hdrDiagnostics: hdrDiagnostics.videoExtractionFailures > 0 || hdrDiagnostics.imageDecodeFailures > 0 ? { ...hdrDiagnostics } : void 0,
35478
35921
  captureAvgMs: totalFrames > 0 ? Math.round((perfStages.captureMs ?? 0) / totalFrames) : void 0,
35479
35922
  peakRssMb: Math.round(peakRssBytes / (1024 * 1024)),
@@ -35589,7 +36032,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
35589
36032
  clearInterval(memSamplerInterval);
35590
36033
  }
35591
36034
  }
35592
- var RenderCancelledError, BROWSER_MEDIA_EPSILON;
36035
+ var RenderCancelledError, BROWSER_MEDIA_EPSILON, CAPTURE_CALIBRATION_TARGET_MS, MAX_MEASURED_CAPTURE_COST_MULTIPLIER, CAPTURE_CALIBRATION_PROTOCOL_TIMEOUT_MS;
35593
36036
  var init_renderOrchestrator = __esm({
35594
36037
  "../producer/src/services/renderOrchestrator.ts"() {
35595
36038
  "use strict";
@@ -35610,6 +36053,9 @@ var init_renderOrchestrator = __esm({
35610
36053
  }
35611
36054
  };
35612
36055
  BROWSER_MEDIA_EPSILON = 1e-4;
36056
+ CAPTURE_CALIBRATION_TARGET_MS = 600;
36057
+ MAX_MEASURED_CAPTURE_COST_MULTIPLIER = 8;
36058
+ CAPTURE_CALIBRATION_PROTOCOL_TIMEOUT_MS = 3e4;
35613
36059
  }
35614
36060
  });
35615
36061
 
@@ -36488,23 +36934,34 @@ function createStudioServer(options) {
36488
36934
  await new Promise((r2) => setTimeout(r2, 200));
36489
36935
  let clip;
36490
36936
  if (opts.selector) {
36491
- clip = await page.evaluate((selector) => {
36492
- const el = document.querySelector(selector);
36493
- if (!(el instanceof HTMLElement)) return void 0;
36494
- const rect = el.getBoundingClientRect();
36495
- if (rect.width < 4 || rect.height < 4) return void 0;
36496
- const pad = 8;
36497
- const x4 = Math.max(0, rect.left - pad);
36498
- const y2 = Math.max(0, rect.top - pad);
36499
- const maxWidth = window.innerWidth - x4;
36500
- const maxHeight = window.innerHeight - y2;
36501
- return {
36502
- x: x4,
36503
- y: y2,
36504
- width: Math.max(1, Math.min(rect.width + pad * 2, maxWidth)),
36505
- height: Math.max(1, Math.min(rect.height + pad * 2, maxHeight))
36506
- };
36507
- }, opts.selector);
36937
+ clip = await page.evaluate(
36938
+ (selector, selectorIndex) => {
36939
+ const matches2 = Array.from(document.querySelectorAll(selector)).filter(
36940
+ (el2) => el2 instanceof HTMLElement
36941
+ );
36942
+ const safeIndex = Math.max(
36943
+ 0,
36944
+ Math.min(matches2.length - 1, Math.floor(selectorIndex ?? 0))
36945
+ );
36946
+ const el = matches2[safeIndex] ?? null;
36947
+ if (!(el instanceof HTMLElement)) return void 0;
36948
+ const rect = el.getBoundingClientRect();
36949
+ if (rect.width < 4 || rect.height < 4) return void 0;
36950
+ const pad = 8;
36951
+ const x4 = Math.max(0, rect.left - pad);
36952
+ const y2 = Math.max(0, rect.top - pad);
36953
+ const maxWidth = window.innerWidth - x4;
36954
+ const maxHeight = window.innerHeight - y2;
36955
+ return {
36956
+ x: x4,
36957
+ y: y2,
36958
+ width: Math.max(1, Math.min(rect.width + pad * 2, maxWidth)),
36959
+ height: Math.max(1, Math.min(rect.height + pad * 2, maxHeight))
36960
+ };
36961
+ },
36962
+ opts.selector,
36963
+ opts.selectorIndex
36964
+ );
36508
36965
  }
36509
36966
  const screenshot = await page.screenshot({
36510
36967
  type: "jpeg",
@@ -38631,13 +39088,13 @@ function buildDockerRunArgs(input) {
38631
39088
  options.quality,
38632
39089
  "--format",
38633
39090
  options.format,
38634
- "--workers",
38635
- String(options.workers),
39091
+ ...options.workers != null ? ["--workers", String(options.workers)] : [],
38636
39092
  ...options.crf != null ? ["--crf", String(options.crf)] : [],
38637
39093
  ...options.videoBitrate ? ["--video-bitrate", options.videoBitrate] : [],
38638
39094
  ...options.quiet ? ["--quiet"] : [],
38639
39095
  ...options.gpu ? ["--gpu"] : [],
38640
- ...options.hdr ? ["--hdr"] : []
39096
+ ...options.hdrMode === "force-hdr" ? ["--hdr"] : [],
39097
+ ...options.hdrMode === "force-sdr" ? ["--sdr"] : []
38641
39098
  ];
38642
39099
  }
38643
39100
  var init_dockerRunArgs = __esm({
@@ -38693,9 +39150,6 @@ import { mkdirSync as mkdirSync24, readFileSync as readFileSync29, statSync as s
38693
39150
  import { cpus as cpus3, freemem as freemem3, tmpdir as tmpdir4 } from "os";
38694
39151
  import { resolve as resolve28, dirname as dirname16, join as join44, basename as basename9 } from "path";
38695
39152
  import { execFileSync as execFileSync6, spawn as spawn11 } from "child_process";
38696
- function defaultWorkerCount() {
38697
- return Math.max(1, Math.min(Math.floor(CPU_CORE_COUNT * 3 / 4), 8));
38698
- }
38699
39153
  function dockerImageTag(version) {
38700
39154
  return `${DOCKER_IMAGE_PREFIX}:${version}`;
38701
39155
  }
@@ -38787,7 +39241,7 @@ async function renderDocker(projectDir, outputPath, options) {
38787
39241
  format: options.format,
38788
39242
  workers: options.workers,
38789
39243
  gpu: options.gpu,
38790
- hdr: options.hdr,
39244
+ hdrMode: options.hdrMode,
38791
39245
  crf: options.crf,
38792
39246
  videoBitrate: options.videoBitrate,
38793
39247
  quiet: options.quiet
@@ -38836,7 +39290,7 @@ async function renderLocal(projectDir, outputPath, options) {
38836
39290
  format: options.format,
38837
39291
  workers: options.workers,
38838
39292
  useGpu: options.gpu,
38839
- hdr: options.hdr,
39293
+ hdrMode: options.hdrMode,
38840
39294
  crf: options.crf,
38841
39295
  videoBitrate: options.videoBitrate
38842
39296
  });
@@ -38883,7 +39337,7 @@ function trackRenderMetrics(job, elapsedMs, options, docker) {
38883
39337
  durationMs: elapsedMs,
38884
39338
  fps: options.fps,
38885
39339
  quality: options.quality,
38886
- workers: options.workers,
39340
+ workers: options.workers ?? perf?.workers,
38887
39341
  docker,
38888
39342
  gpu: options.gpu,
38889
39343
  compositionDurationMs,
@@ -38949,7 +39403,7 @@ var init_render2 = __esm({
38949
39403
  ["High quality at 60fps", "hyperframes render --fps 60 --quality high --output hd.mp4"],
38950
39404
  ["Deterministic render via Docker", "hyperframes render --docker --output deterministic.mp4"],
38951
39405
  ["Parallel rendering with 6 workers", "hyperframes render --workers 6 --output fast.mp4"],
38952
- ["HDR output (H.265 10-bit)", "hyperframes render --hdr --output hdr-output.mp4"]
39406
+ ["HDR output (auto-detected)", "hyperframes render --output hdr-output.mp4"]
38953
39407
  ];
38954
39408
  VALID_FPS = /* @__PURE__ */ new Set([24, 30, 60]);
38955
39409
  VALID_QUALITY = /* @__PURE__ */ new Set(["draft", "standard", "high"]);
@@ -39001,7 +39455,12 @@ var init_render2 = __esm({
39001
39455
  },
39002
39456
  hdr: {
39003
39457
  type: "boolean",
39004
- description: "Enable HDR: probe sources for PQ/HLG, output H.265 10-bit BT.2020",
39458
+ description: "Force HDR output even if no HDR sources are detected",
39459
+ default: false
39460
+ },
39461
+ sdr: {
39462
+ type: "boolean",
39463
+ description: "Force SDR output even if HDR sources are detected",
39005
39464
  default: false
39006
39465
  },
39007
39466
  crf: {
@@ -39107,9 +39566,8 @@ var init_render2 = __esm({
39107
39566
  );
39108
39567
  process.exit(1);
39109
39568
  }
39110
- const workerCount = workers ?? defaultWorkerCount();
39111
39569
  if (!quiet) {
39112
- const workerLabel = args.workers != null ? `${workerCount} workers` : `${workerCount} workers (auto \u2014 ${CPU_CORE_COUNT} cores detected)`;
39570
+ const workerLabel = workers != null ? `${workers} workers` : `auto workers (${CPU_CORE_COUNT} cores detected)`;
39113
39571
  console.log("");
39114
39572
  console.log(
39115
39573
  c.accent("\u25C6") + " Rendering " + c.accent(project.name) + c.dim(" \u2192 " + outputPath)
@@ -39177,14 +39635,18 @@ var init_render2 = __esm({
39177
39635
  console.log("");
39178
39636
  }
39179
39637
  }
39638
+ if (args.hdr && args.sdr) {
39639
+ console.error("Error: --hdr and --sdr are mutually exclusive.");
39640
+ process.exit(1);
39641
+ }
39180
39642
  if (useDocker) {
39181
39643
  await renderDocker(project.dir, outputPath, {
39182
39644
  fps,
39183
39645
  quality,
39184
39646
  format,
39185
- workers: workerCount,
39647
+ workers,
39186
39648
  gpu: useGpu,
39187
- hdr: args.hdr ?? false,
39649
+ hdrMode: args.sdr ? "force-sdr" : args.hdr ? "force-hdr" : "auto",
39188
39650
  crf,
39189
39651
  videoBitrate,
39190
39652
  quiet
@@ -39194,9 +39656,9 @@ var init_render2 = __esm({
39194
39656
  fps,
39195
39657
  quality,
39196
39658
  format,
39197
- workers: workerCount,
39659
+ workers,
39198
39660
  gpu: useGpu,
39199
- hdr: args.hdr ?? false,
39661
+ hdrMode: args.sdr ? "force-sdr" : args.hdr ? "force-hdr" : "auto",
39200
39662
  crf,
39201
39663
  videoBitrate,
39202
39664
  quiet,