hyperframes 0.8.22 → 0.8.24
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 +273 -30
- package/dist/fontLocalizeCli.js +199 -8
- package/dist/hyperframes-player.global.js +1 -1
- package/dist/skills/hyperframes-cli/references/lint-validate-inspect.md +1 -1
- package/dist/studio/assets/{hyperframes-player-DmtkkaPN.js → hyperframes-player-CTQCs_KG.js} +1 -1
- package/dist/studio/assets/{index-DXR3nsh2.js → index-9WJFcXTP.js} +1 -1
- package/dist/studio/assets/{index-BIf0zQNt.js → index-Bq3M0sjr.js} +3 -3
- package/dist/studio/assets/{index-DivnGS5G.js → index-okdE01ky.js} +1 -1
- package/dist/studio/index.html +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -50,7 +50,7 @@ var VERSION;
|
|
|
50
50
|
var init_version = __esm({
|
|
51
51
|
"src/version.ts"() {
|
|
52
52
|
"use strict";
|
|
53
|
-
VERSION = true ? "0.8.
|
|
53
|
+
VERSION = true ? "0.8.24" : "0.0.0-dev";
|
|
54
54
|
}
|
|
55
55
|
});
|
|
56
56
|
|
|
@@ -73411,7 +73411,22 @@ async function downloadWithRetry(url, localPath, timeoutMs, signal, onTransientR
|
|
|
73411
73411
|
return await runDownloadAttempt(url, localPath, timeoutMs, attempt + 1, options, signal);
|
|
73412
73412
|
} catch (error) {
|
|
73413
73413
|
const classified = classifyDownloadFailure(error);
|
|
73414
|
-
if (!classified.locallyRetryable || attempt >= maxTransientRetries)
|
|
73414
|
+
if (!classified.locallyRetryable || attempt >= maxTransientRetries) {
|
|
73415
|
+
const identity2 = safeDownloadUrlIdentity(url);
|
|
73416
|
+
throw new UrlDownloadError(
|
|
73417
|
+
classified.kind,
|
|
73418
|
+
classified.retryable,
|
|
73419
|
+
classified.message,
|
|
73420
|
+
classified.status,
|
|
73421
|
+
{
|
|
73422
|
+
...classified.telemetry,
|
|
73423
|
+
urlFingerprint: identity2.urlFingerprint,
|
|
73424
|
+
initialHost: identity2.host,
|
|
73425
|
+
attempt: attempt + 1
|
|
73426
|
+
},
|
|
73427
|
+
classified.locallyRetryable
|
|
73428
|
+
);
|
|
73429
|
+
}
|
|
73415
73430
|
if (classified.retryable) onTransientRetry?.(classified);
|
|
73416
73431
|
const identity = safeDownloadUrlIdentity(url);
|
|
73417
73432
|
emitDownloadTelemetry(options, {
|
|
@@ -84035,6 +84050,47 @@ ${other.raw}`)
|
|
|
84035
84050
|
}
|
|
84036
84051
|
return findings;
|
|
84037
84052
|
},
|
|
84053
|
+
// gsap_timeline_return_used_as_tween — `tl.to()` returns the TIMELINE, not the tween it just
|
|
84054
|
+
// created. `gsap.to()` returns a Tween, so the two read identically and behave nothing alike.
|
|
84055
|
+
// Capturing the timeline's return and later treating it as an individual animation aims a
|
|
84056
|
+
// tween-scoped call at the whole composition: `.kill()` on it interrupts the master timeline
|
|
84057
|
+
// and detaches it from its parent, which stops a parent-driven seek dead while leaving an
|
|
84058
|
+
// explicit `tl.progress()` still working -- so a render of the packaged defaults looks fine
|
|
84059
|
+
// and only live playback breaks. A rebuild that collected these to discard them also leaves
|
|
84060
|
+
// every previous keyframe in place and stacks the new ones on top.
|
|
84061
|
+
({ scripts }) => {
|
|
84062
|
+
const findings = [];
|
|
84063
|
+
for (const script of scripts) {
|
|
84064
|
+
const source = stripJsComments(script.content);
|
|
84065
|
+
const timelineVars = collectTimelineVarNames(source);
|
|
84066
|
+
if (timelineVars.length === 0) continue;
|
|
84067
|
+
const receivers = timelineVars.map(escapeRegExp22).join("|");
|
|
84068
|
+
const pattern = new RegExp(
|
|
84069
|
+
String.raw`(?:\.push\s*\(\s*|(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*)(?:${receivers})\s*\.\s*(to|from|fromTo|set|call|add)\s*\(`,
|
|
84070
|
+
"g"
|
|
84071
|
+
);
|
|
84072
|
+
let match;
|
|
84073
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
84074
|
+
const boundName = match[1];
|
|
84075
|
+
const method = match[2];
|
|
84076
|
+
if (boundName) {
|
|
84077
|
+
const treatedAsTween = new RegExp(
|
|
84078
|
+
String.raw`\b${escapeRegExp22(boundName)}\s*\.\s*(?:kill|revert|invalidate|pause|resume|restart|seek|progress|timeScale)\s*\(`
|
|
84079
|
+
);
|
|
84080
|
+
if (!treatedAsTween.test(source)) continue;
|
|
84081
|
+
}
|
|
84082
|
+
const contextStart = Math.max(0, match.index - 40);
|
|
84083
|
+
findings.push({
|
|
84084
|
+
code: "gsap_timeline_return_used_as_tween",
|
|
84085
|
+
severity: "error",
|
|
84086
|
+
message: `\`${match[0].includes(".push") ? "push" : boundName}\` captures the return of \`.${method}()\` on timeline \`${timelineVars[0]}\`, but a timeline's \`.to()\`/\`.from()\`/\`.set()\` returns THE TIMELINE ITSELF, not the tween it created. Every captured value is the same master timeline, so a tween-scoped call on it \u2014 \`.kill()\` above all \u2014 hits the whole composition: it interrupts the timeline and detaches it from its parent, which freezes a parent-driven seek while an explicit \`tl.progress()\` keeps working. Only \`gsap.to()\` returns a tween.`,
|
|
84087
|
+
fixHint: "To build a group of keyframes you can later discard, put them in a nested timeline: `const nested = gsap.timeline(); nested.to(...); tl.add(nested, 0);` \u2014 then `nested.kill()` replaces exactly those keyframes and cannot reach `tl`. Children keep their absolute times when the nest is added at 0. To hold a single real tween, create it with `gsap.to(...)` and place it with `tl.add(tween, at)`.",
|
|
84088
|
+
snippet: truncateSnippet(source.slice(contextStart, match.index + match[0].length + 60))
|
|
84089
|
+
});
|
|
84090
|
+
}
|
|
84091
|
+
}
|
|
84092
|
+
return findings;
|
|
84093
|
+
},
|
|
84038
84094
|
// gsap_group_selector_keyframes
|
|
84039
84095
|
({ scripts }) => {
|
|
84040
84096
|
const findings = [];
|
|
@@ -87208,6 +87264,37 @@ function extractionFrameCountForDuration(durationSeconds, fps, isVFR) {
|
|
|
87208
87264
|
function sdrToHdrTransformKey(transfer) {
|
|
87209
87265
|
return `sdr2hdr-${transfer}`;
|
|
87210
87266
|
}
|
|
87267
|
+
function safeVideoExtractionSourceIdentity(source) {
|
|
87268
|
+
if (!isHttpUrl(source)) return null;
|
|
87269
|
+
const identity = safeDownloadUrlIdentity(source);
|
|
87270
|
+
return {
|
|
87271
|
+
sourceFingerprint: `sha256:${identity.urlFingerprint}`,
|
|
87272
|
+
host: identity.host ?? "other"
|
|
87273
|
+
};
|
|
87274
|
+
}
|
|
87275
|
+
function downloadStatusClass(error) {
|
|
87276
|
+
const status = error.status ?? error.telemetry?.status;
|
|
87277
|
+
if (typeof status === "number" && status >= 400 && status < 500) return "http_4xx";
|
|
87278
|
+
if (typeof status === "number" && status >= 500 && status < 600) return "http_5xx";
|
|
87279
|
+
if (error.kind === "timeout") return "timeout";
|
|
87280
|
+
if (error.kind === "network") return "network";
|
|
87281
|
+
return "other";
|
|
87282
|
+
}
|
|
87283
|
+
function downloadFailureGroup(source, error) {
|
|
87284
|
+
const sourceIdentity = safeVideoExtractionSourceIdentity(source);
|
|
87285
|
+
const failureHost = error.telemetry?.finalHost ?? error.telemetry?.initialHost;
|
|
87286
|
+
const attempt = error.telemetry?.attempt;
|
|
87287
|
+
return {
|
|
87288
|
+
...sourceIdentity ? { sourceFingerprint: sourceIdentity.sourceFingerprint } : {},
|
|
87289
|
+
...failureHost ?? sourceIdentity?.host ? { host: failureHost ?? sourceIdentity?.host } : {},
|
|
87290
|
+
statusClass: downloadStatusClass(error),
|
|
87291
|
+
retry: {
|
|
87292
|
+
phase: "download",
|
|
87293
|
+
used: typeof attempt === "number" && attempt >= 2 ? 1 : 0,
|
|
87294
|
+
budget: 1
|
|
87295
|
+
}
|
|
87296
|
+
};
|
|
87297
|
+
}
|
|
87211
87298
|
function isVideoSourceExtractionError(error) {
|
|
87212
87299
|
return typeof error === "object" && error !== null && "hyperframesVideoSourceExtractionError" in error && error.hyperframesVideoSourceExtractionError === true;
|
|
87213
87300
|
}
|
|
@@ -87999,6 +88086,7 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config, c
|
|
|
87999
88086
|
videoId: video.id,
|
|
88000
88087
|
kind: classified.kind,
|
|
88001
88088
|
retryable: classified.retryable,
|
|
88089
|
+
...err instanceof UrlDownloadError ? { group: downloadFailureGroup(video.src, err) } : {},
|
|
88002
88090
|
error: classified.diagnostic
|
|
88003
88091
|
});
|
|
88004
88092
|
}
|
|
@@ -93096,6 +93184,7 @@ __export(src_exports, {
|
|
|
93096
93184
|
runFfmpeg: () => runFfmpeg,
|
|
93097
93185
|
runVideoExtractionWithRetry: () => runVideoExtractionWithRetry,
|
|
93098
93186
|
safeDownloadUrlIdentity: () => safeDownloadUrlIdentity,
|
|
93187
|
+
safeVideoExtractionSourceIdentity: () => safeVideoExtractionSourceIdentity,
|
|
93099
93188
|
sampleRgb48le: () => sampleRgb48le,
|
|
93100
93189
|
scaleProtocolTimeoutForComposition: () => scaleProtocolTimeoutForComposition,
|
|
93101
93190
|
selectVerifySampleIndicesForTask: () => selectVerifySampleIndicesForTask,
|
|
@@ -125699,12 +125788,88 @@ var init_planValidation = __esm({
|
|
|
125699
125788
|
}
|
|
125700
125789
|
});
|
|
125701
125790
|
|
|
125791
|
+
// ../producer/src/services/render/extractionFailureMetadata.ts
|
|
125792
|
+
function stringOrEmpty(value) {
|
|
125793
|
+
return value === void 0 ? "" : value;
|
|
125794
|
+
}
|
|
125795
|
+
function retrySortTuple(retry) {
|
|
125796
|
+
return retry === void 0 ? ["", -1, -1] : [retry.phase, retry.used, retry.budget];
|
|
125797
|
+
}
|
|
125798
|
+
function extractionFailureGroupSortTuple(group) {
|
|
125799
|
+
const retry = retrySortTuple(group.retry);
|
|
125800
|
+
return [
|
|
125801
|
+
group.kind,
|
|
125802
|
+
stringOrEmpty(group.sourceFingerprint),
|
|
125803
|
+
stringOrEmpty(group.host),
|
|
125804
|
+
stringOrEmpty(group.statusClass),
|
|
125805
|
+
retry[0],
|
|
125806
|
+
retry[1],
|
|
125807
|
+
retry[2]
|
|
125808
|
+
];
|
|
125809
|
+
}
|
|
125810
|
+
function compareExtractionFailureGroups(left, right) {
|
|
125811
|
+
const leftTuple = extractionFailureGroupSortTuple(left);
|
|
125812
|
+
const rightTuple = extractionFailureGroupSortTuple(right);
|
|
125813
|
+
for (let index = 0; index < leftTuple.length; index += 1) {
|
|
125814
|
+
const leftValue = leftTuple[index];
|
|
125815
|
+
const rightValue = rightTuple[index];
|
|
125816
|
+
if (leftValue < rightValue) return -1;
|
|
125817
|
+
if (leftValue > rightValue) return 1;
|
|
125818
|
+
}
|
|
125819
|
+
return 0;
|
|
125820
|
+
}
|
|
125821
|
+
function extractionFailureGroupIdentityKey(group) {
|
|
125822
|
+
return JSON.stringify(extractionFailureGroupSortTuple(group));
|
|
125823
|
+
}
|
|
125824
|
+
var init_extractionFailureMetadata = __esm({
|
|
125825
|
+
"../producer/src/services/render/extractionFailureMetadata.ts"() {
|
|
125826
|
+
"use strict";
|
|
125827
|
+
}
|
|
125828
|
+
});
|
|
125829
|
+
|
|
125702
125830
|
// ../producer/src/services/render/stages/extractVideosStage.ts
|
|
125703
125831
|
import { existsSync as existsSync51 } from "fs";
|
|
125704
125832
|
import { join as join50 } from "path";
|
|
125705
125833
|
function shouldCopyExtractedFrames(platform11) {
|
|
125706
125834
|
return platform11 === "win32";
|
|
125707
125835
|
}
|
|
125836
|
+
function buildExtractionFailureMetadata(inputs) {
|
|
125837
|
+
const kindCounts = /* @__PURE__ */ new Map();
|
|
125838
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
125839
|
+
for (const input2 of inputs) {
|
|
125840
|
+
kindCounts.set(input2.kind, (kindCounts.get(input2.kind) ?? 0) + input2.affectedElementCount);
|
|
125841
|
+
const key2 = extractionFailureGroupIdentityKey(input2);
|
|
125842
|
+
const existing = grouped.get(key2);
|
|
125843
|
+
if (existing) {
|
|
125844
|
+
existing.affectedElementCount += input2.affectedElementCount;
|
|
125845
|
+
} else {
|
|
125846
|
+
grouped.set(key2, { ...input2 });
|
|
125847
|
+
}
|
|
125848
|
+
}
|
|
125849
|
+
const allGroups = [...grouped.values()].sort(compareExtractionFailureGroups);
|
|
125850
|
+
return {
|
|
125851
|
+
schemaVersion: 1,
|
|
125852
|
+
kindCounts: [...kindCounts].map(([kind, affectedElementCount]) => ({
|
|
125853
|
+
kind,
|
|
125854
|
+
affectedElementCount
|
|
125855
|
+
})).sort((a, b2) => a.kind < b2.kind ? -1 : a.kind > b2.kind ? 1 : 0),
|
|
125856
|
+
groups: allGroups.slice(0, MAX_EXTRACTION_FAILURE_GROUPS),
|
|
125857
|
+
omittedGroupCount: Math.max(0, allGroups.length - MAX_EXTRACTION_FAILURE_GROUPS)
|
|
125858
|
+
};
|
|
125859
|
+
}
|
|
125860
|
+
function extractionFailureMetadataFromResult(result) {
|
|
125861
|
+
return buildExtractionFailureMetadata(
|
|
125862
|
+
result.errors.map((failure) => ({
|
|
125863
|
+
kind: failure.kind ?? "internal",
|
|
125864
|
+
affectedElementCount: 1,
|
|
125865
|
+
...failure.group
|
|
125866
|
+
}))
|
|
125867
|
+
);
|
|
125868
|
+
}
|
|
125869
|
+
function safeVideoExtractionSourceLogMetadata(source) {
|
|
125870
|
+
const identity = safeVideoExtractionSourceIdentity(source);
|
|
125871
|
+
return identity ? { sourceType: "remote", ...identity } : { sourceType: "local" };
|
|
125872
|
+
}
|
|
125708
125873
|
function resolveVideoExtractionPolicy(env = process.env) {
|
|
125709
125874
|
const rawMode = env.HF_VIDEO_EXTRACTION_FAILURE_MODE?.trim().toLowerCase();
|
|
125710
125875
|
const failureMode = rawMode === "observe" || rawMode === "off" ? rawMode : "enforce";
|
|
@@ -125738,7 +125903,8 @@ function buildVideoExtractionStageError(result) {
|
|
|
125738
125903
|
return new VideoExtractionStageError(
|
|
125739
125904
|
retryable ? "VIDEO_EXTRACTION_FAILED" : "VIDEO_SOURCE_UNRENDERABLE",
|
|
125740
125905
|
retryable,
|
|
125741
|
-
failures
|
|
125906
|
+
failures,
|
|
125907
|
+
extractionFailureMetadataFromResult(result)
|
|
125742
125908
|
);
|
|
125743
125909
|
}
|
|
125744
125910
|
function buildHdrProbeStageError(failures) {
|
|
@@ -125753,7 +125919,13 @@ function buildHdrProbeStageError(failures) {
|
|
|
125753
125919
|
return new VideoExtractionStageError(
|
|
125754
125920
|
retryable ? "VIDEO_EXTRACTION_FAILED" : "VIDEO_SOURCE_UNRENDERABLE",
|
|
125755
125921
|
retryable,
|
|
125756
|
-
summary
|
|
125922
|
+
summary,
|
|
125923
|
+
buildExtractionFailureMetadata(
|
|
125924
|
+
failures.map((failure) => ({
|
|
125925
|
+
kind: failure.kind,
|
|
125926
|
+
affectedElementCount: 1
|
|
125927
|
+
}))
|
|
125928
|
+
)
|
|
125757
125929
|
);
|
|
125758
125930
|
}
|
|
125759
125931
|
function isHdrProbeFailure(failure) {
|
|
@@ -125880,7 +126052,10 @@ async function runExtractVideosStage(input2) {
|
|
|
125880
126052
|
const totalVideos = composition.videos.length;
|
|
125881
126053
|
for (let i2 = 0; i2 < totalVideos; i2++) {
|
|
125882
126054
|
const v2 = composition.videos[i2];
|
|
125883
|
-
log2?.info(
|
|
126055
|
+
log2?.info(
|
|
126056
|
+
`Extracting frames from video ${i2 + 1}/${totalVideos}`,
|
|
126057
|
+
safeVideoExtractionSourceLogMetadata(v2.src)
|
|
126058
|
+
);
|
|
125884
126059
|
}
|
|
125885
126060
|
extractionResult = await extractAllVideoFrames(
|
|
125886
126061
|
composition.videos,
|
|
@@ -125952,7 +126127,7 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
|
|
|
125952
126127
|
existingAudioSrcs.add(video.src);
|
|
125953
126128
|
}
|
|
125954
126129
|
}
|
|
125955
|
-
var VideoExtractionStageError;
|
|
126130
|
+
var MAX_EXTRACTION_FAILURE_GROUPS, VideoExtractionStageError;
|
|
125956
126131
|
var init_extractVideosStage = __esm({
|
|
125957
126132
|
"../producer/src/services/render/stages/extractVideosStage.ts"() {
|
|
125958
126133
|
"use strict";
|
|
@@ -125960,19 +126135,35 @@ var init_extractVideosStage = __esm({
|
|
|
125960
126135
|
init_renderOrchestrator();
|
|
125961
126136
|
init_shared();
|
|
125962
126137
|
init_encoderInterruption();
|
|
126138
|
+
init_extractionFailureMetadata();
|
|
126139
|
+
MAX_EXTRACTION_FAILURE_GROUPS = 8;
|
|
125963
126140
|
VideoExtractionStageError = class extends Error {
|
|
125964
|
-
constructor(code, retryable, failures
|
|
126141
|
+
constructor(code, retryable, failures, extractionFailure = buildExtractionFailureMetadata(
|
|
126142
|
+
failures.map((failure) => ({
|
|
126143
|
+
kind: failure.kind,
|
|
126144
|
+
affectedElementCount: failure.count
|
|
126145
|
+
}))
|
|
126146
|
+
)) {
|
|
125965
126147
|
const total = failures.reduce((sum, failure) => sum + failure.count, 0);
|
|
125966
126148
|
const breakdown = failures.map((failure) => `${failure.kind}=${failure.count}`).join(",");
|
|
125967
126149
|
super(`Video extraction failed for ${total} source(s) [${code}; ${breakdown}]`);
|
|
125968
126150
|
this.code = code;
|
|
125969
126151
|
this.retryable = retryable;
|
|
125970
126152
|
this.failures = failures;
|
|
126153
|
+
this.extractionFailure = extractionFailure;
|
|
125971
126154
|
this.name = "VideoExtractionStageError";
|
|
126155
|
+
this.publicMetadata = { extractionFailure };
|
|
125972
126156
|
}
|
|
125973
126157
|
code;
|
|
125974
126158
|
retryable;
|
|
125975
126159
|
failures;
|
|
126160
|
+
extractionFailure;
|
|
126161
|
+
/**
|
|
126162
|
+
* Producer servers may expose this explicitly public, JSON-compatible data
|
|
126163
|
+
* without knowing its schema. Boundary adapters remain responsible for
|
|
126164
|
+
* validating and applying policy to it.
|
|
126165
|
+
*/
|
|
126166
|
+
publicMetadata;
|
|
125976
126167
|
};
|
|
125977
126168
|
}
|
|
125978
126169
|
});
|
|
@@ -132280,10 +132471,12 @@ function extractSafeRenderErrorMetadata(error) {
|
|
|
132280
132471
|
if (!errorCode || typeof error !== "object" || error === null) return void 0;
|
|
132281
132472
|
const owner = "owner" in error ? error.owner : void 0;
|
|
132282
132473
|
const retryable = "retryable" in error ? error.retryable : void 0;
|
|
132474
|
+
const publicMetadata = "publicMetadata" in error ? error.publicMetadata : void 0;
|
|
132283
132475
|
return {
|
|
132284
132476
|
errorCode,
|
|
132285
132477
|
errorOwner: owner === "user" || owner === "system" ? owner : void 0,
|
|
132286
|
-
retryable: typeof retryable === "boolean" ? retryable : void 0
|
|
132478
|
+
retryable: typeof retryable === "boolean" ? retryable : void 0,
|
|
132479
|
+
...isPlainObject4(publicMetadata) ? { errorMetadata: publicMetadata } : {}
|
|
132287
132480
|
};
|
|
132288
132481
|
}
|
|
132289
132482
|
function parseServerFps(value) {
|
|
@@ -132605,6 +132798,7 @@ async function writeRenderStreamFailure(input2) {
|
|
|
132605
132798
|
errorCode: safeError?.errorCode,
|
|
132606
132799
|
errorOwner: safeError?.errorOwner,
|
|
132607
132800
|
retryable: safeError?.retryable,
|
|
132801
|
+
errorMetadata: safeError?.errorMetadata,
|
|
132608
132802
|
stage: job.currentStage,
|
|
132609
132803
|
elapsedMs,
|
|
132610
132804
|
errorDetails: job.errorDetails ?? null,
|
|
@@ -132730,6 +132924,7 @@ function createRenderHandlers(options = {}) {
|
|
|
132730
132924
|
errorCode: safeError?.errorCode,
|
|
132731
132925
|
errorOwner: safeError?.errorOwner,
|
|
132732
132926
|
retryable: safeError?.retryable,
|
|
132927
|
+
errorMetadata: safeError?.errorMetadata,
|
|
132733
132928
|
stage: job.currentStage,
|
|
132734
132929
|
durationMs,
|
|
132735
132930
|
errorDetails: job.errorDetails ?? null
|
|
@@ -149583,18 +149778,22 @@ function geometryIssueAnchor(candidate, time) {
|
|
|
149583
149778
|
rect: candidate.rect
|
|
149584
149779
|
};
|
|
149585
149780
|
}
|
|
149586
|
-
function
|
|
149587
|
-
const
|
|
149588
|
-
const
|
|
149589
|
-
const
|
|
149590
|
-
|
|
149781
|
+
function captionBoxOverlapsZone(box, zone, canvas) {
|
|
149782
|
+
const zx0 = zone.x0 * canvas.width;
|
|
149783
|
+
const zy0 = zone.y0 * canvas.height;
|
|
149784
|
+
const zx1 = zone.x1 * canvas.width;
|
|
149785
|
+
const zy1 = zone.y1 * canvas.height;
|
|
149786
|
+
const bx1 = box.left + box.width;
|
|
149787
|
+
const by1 = box.top + box.height;
|
|
149788
|
+
const overlaps = box.left < zx1 && bx1 > zx0 && box.top < zy1 && by1 > zy0;
|
|
149789
|
+
return { overlaps, cy: box.top + box.height / 2 };
|
|
149591
149790
|
}
|
|
149592
149791
|
function captionFinding(candidate, options, canvas, time) {
|
|
149593
149792
|
const zone = options.captionZone;
|
|
149594
149793
|
if (!zone || candidate.kind !== "text" || !candidateIsSized(candidate, canvas)) return null;
|
|
149595
149794
|
if ("data-layout-allow-caption-zone" in candidate.dataAttributes) return null;
|
|
149596
|
-
const {
|
|
149597
|
-
if (!
|
|
149795
|
+
const { overlaps, cy } = captionBoxOverlapsZone(candidate.elementRect, zone, canvas);
|
|
149796
|
+
if (!overlaps) return null;
|
|
149598
149797
|
const text2 = candidate.text.slice(0, 48);
|
|
149599
149798
|
const pctFromBottom = Math.round((canvas.height - cy) / canvas.height * 100);
|
|
149600
149799
|
return {
|
|
@@ -149604,7 +149803,7 @@ function captionFinding(candidate, options, canvas, time) {
|
|
|
149604
149803
|
code: "caption_zone_collision",
|
|
149605
149804
|
severity: zone.severity === "error" ? "error" : "warning",
|
|
149606
149805
|
text: text2,
|
|
149607
|
-
message: `<${candidate.tag}> "${text2}"
|
|
149806
|
+
message: `<${candidate.tag}> "${text2}" overlaps the reserved caption band (~${pctFromBottom}% up from the bottom).`,
|
|
149608
149807
|
fixHint: "Keep main content outside the configured caption band, or mark intentional lower-third copy with data-layout-allow-caption-zone."
|
|
149609
149808
|
}
|
|
149610
149809
|
};
|
|
@@ -203174,18 +203373,25 @@ async function captionImagesWithGemini(outputDir, progress, warnings, options =
|
|
|
203174
203373
|
}
|
|
203175
203374
|
const openRouterKey = process.env.OPENROUTER_API_KEY;
|
|
203176
203375
|
const geminiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
203177
|
-
|
|
203376
|
+
const vertexProject = process.env.HYPERFRAMES_VERTEX_PROJECT_ID;
|
|
203377
|
+
const vertexServiceAccount = process.env.HYPERFRAMES_VERTEX_SERVICE_ACCOUNT;
|
|
203378
|
+
const useVertex = Boolean(vertexProject && vertexServiceAccount);
|
|
203379
|
+
if (!openRouterKey && !useVertex && !geminiKey) {
|
|
203178
203380
|
reportOutcome();
|
|
203179
203381
|
return geminiCaptions;
|
|
203180
203382
|
}
|
|
203181
|
-
const
|
|
203182
|
-
const providerName =
|
|
203183
|
-
const model =
|
|
203383
|
+
const provider = openRouterKey ? "openrouter" : useVertex ? "vertex" : "gemini";
|
|
203384
|
+
const providerName = { openrouter: "OpenRouter", vertex: "Vertex AI", gemini: "Gemini" }[provider];
|
|
203385
|
+
const model = {
|
|
203386
|
+
openrouter: process.env.HYPERFRAMES_OPENROUTER_MODEL || "google/gemini-3.1-flash-lite",
|
|
203387
|
+
vertex: process.env.HYPERFRAMES_VERTEX_MODEL || "gemini-2.5-flash",
|
|
203388
|
+
gemini: process.env.HYPERFRAMES_GEMINI_MODEL || "gemini-3.1-flash-lite-preview"
|
|
203389
|
+
}[provider];
|
|
203184
203390
|
const requestTimeoutMs = resolveVisionRequestTimeoutMs();
|
|
203185
203391
|
progress("design", `Captioning images with ${providerName} vision...`);
|
|
203186
203392
|
try {
|
|
203187
203393
|
let captionOne;
|
|
203188
|
-
if (
|
|
203394
|
+
if (provider === "openrouter") {
|
|
203189
203395
|
captionOne = async ({ mimeType, base64, prompt, maxTokens, timeoutMs }) => {
|
|
203190
203396
|
return runBoundedVisionRequest(async (signal) => {
|
|
203191
203397
|
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
@@ -203221,9 +203427,31 @@ async function captionImagesWithGemini(outputDir, progress, warnings, options =
|
|
|
203221
203427
|
}, timeoutMs);
|
|
203222
203428
|
};
|
|
203223
203429
|
} else {
|
|
203224
|
-
if (!geminiKey) return geminiCaptions;
|
|
203225
203430
|
const { GoogleGenAI: GoogleGenAI2 } = await Promise.resolve().then(() => (init_node4(), node_exports));
|
|
203226
|
-
|
|
203431
|
+
let ai;
|
|
203432
|
+
if (provider === "vertex") {
|
|
203433
|
+
if (!vertexProject || !vertexServiceAccount) return geminiCaptions;
|
|
203434
|
+
let credentials;
|
|
203435
|
+
try {
|
|
203436
|
+
credentials = JSON.parse(vertexServiceAccount);
|
|
203437
|
+
} catch {
|
|
203438
|
+
warnings.push(
|
|
203439
|
+
"HYPERFRAMES_VERTEX_SERVICE_ACCOUNT is not valid JSON; skipped vision captioning."
|
|
203440
|
+
);
|
|
203441
|
+
internalError = true;
|
|
203442
|
+
reportOutcome();
|
|
203443
|
+
return geminiCaptions;
|
|
203444
|
+
}
|
|
203445
|
+
ai = new GoogleGenAI2({
|
|
203446
|
+
vertexai: true,
|
|
203447
|
+
project: vertexProject,
|
|
203448
|
+
location: process.env.HYPERFRAMES_VERTEX_LOCATION || "us-central1",
|
|
203449
|
+
googleAuthOptions: { credentials }
|
|
203450
|
+
});
|
|
203451
|
+
} else {
|
|
203452
|
+
if (!geminiKey) return geminiCaptions;
|
|
203453
|
+
ai = new GoogleGenAI2({ apiKey: geminiKey });
|
|
203454
|
+
}
|
|
203227
203455
|
captionOne = async ({ mimeType, base64, prompt, maxTokens, timeoutMs }) => {
|
|
203228
203456
|
const response = await runBoundedVisionRequest(
|
|
203229
203457
|
(signal) => ai.models.generateContent({
|
|
@@ -203236,6 +203464,12 @@ async function captionImagesWithGemini(outputDir, progress, warnings, options =
|
|
|
203236
203464
|
],
|
|
203237
203465
|
config: {
|
|
203238
203466
|
maxOutputTokens: maxTokens,
|
|
203467
|
+
// A one-line factual caption needs no reasoning, and leaving thinking on is
|
|
203468
|
+
// not merely wasteful: thinking tokens are drawn from maxOutputTokens, so the
|
|
203469
|
+
// model can spend the whole budget and return empty text. That surfaces as a
|
|
203470
|
+
// successful request with no caption — the capture then logs "Captioned N/N
|
|
203471
|
+
// images" followed by "0 images captioned", which is what production shows.
|
|
203472
|
+
thinkingConfig: { thinkingBudget: 0 },
|
|
203239
203473
|
abortSignal: signal,
|
|
203240
203474
|
httpOptions: { timeout: timeoutMs }
|
|
203241
203475
|
}
|
|
@@ -203327,6 +203561,7 @@ async function captionImagesWithGemini(outputDir, progress, warnings, options =
|
|
|
203327
203561
|
reportOutcome();
|
|
203328
203562
|
return geminiCaptions;
|
|
203329
203563
|
}
|
|
203564
|
+
const hostConcurrency = sharp3.concurrency();
|
|
203330
203565
|
progress("design", `Rasterizing + captioning ${svgFiles.length} SVGs via vision API...`);
|
|
203331
203566
|
const SVG_BATCH = 20;
|
|
203332
203567
|
const SVG_RENDER_SIZE = 256;
|
|
@@ -203339,10 +203574,11 @@ async function captionImagesWithGemini(outputDir, progress, warnings, options =
|
|
|
203339
203574
|
}
|
|
203340
203575
|
const timeoutMs = Math.max(1, Math.min(requestTimeoutMs, remainingMs));
|
|
203341
203576
|
const batch = svgFiles.slice(i2, i2 + SVG_BATCH);
|
|
203342
|
-
const
|
|
203343
|
-
|
|
203577
|
+
const rasterized = [];
|
|
203578
|
+
sharp3.concurrency(1);
|
|
203579
|
+
try {
|
|
203580
|
+
for (const { relPath } of batch) {
|
|
203344
203581
|
const filePath = join121(assetsDir, relPath);
|
|
203345
|
-
let pngBase64;
|
|
203346
203582
|
try {
|
|
203347
203583
|
const svgSource = readFileSync84(filePath, "utf-8");
|
|
203348
203584
|
const lightFillHits = (svgSource.match(/fill\s*=\s*["'](#fff(fff)?|white|#[ef][ef][ef]|#[ef]{6})["']/gi) || []).length;
|
|
@@ -203354,11 +203590,18 @@ async function captionImagesWithGemini(outputDir, progress, warnings, options =
|
|
|
203354
203590
|
fit: "inside",
|
|
203355
203591
|
withoutEnlargement: false
|
|
203356
203592
|
}).flatten({ background: bg }).png().toBuffer();
|
|
203357
|
-
pngBase64
|
|
203593
|
+
rasterized.push({ relPath, pngBase64: pngBuffer.toString("base64") });
|
|
203358
203594
|
} catch {
|
|
203359
203595
|
svgsSkipped++;
|
|
203360
|
-
|
|
203596
|
+
rasterized.push({ relPath, pngBase64: null });
|
|
203361
203597
|
}
|
|
203598
|
+
}
|
|
203599
|
+
} finally {
|
|
203600
|
+
sharp3.concurrency(hostConcurrency);
|
|
203601
|
+
}
|
|
203602
|
+
const results = await Promise.allSettled(
|
|
203603
|
+
rasterized.map(async ({ relPath, pngBase64 }) => {
|
|
203604
|
+
if (pngBase64 === null) return { file: relPath, caption: "" };
|
|
203362
203605
|
const caption = await captionOne({
|
|
203363
203606
|
mimeType: "image/png",
|
|
203364
203607
|
base64: pngBase64,
|
|
@@ -204810,8 +205053,8 @@ ${extracted.bodyHtml}
|
|
|
204810
205053
|
try {
|
|
204811
205054
|
const lines = generateAssetDescriptions(outputDir, tokens, catalogedAssets, geminiCaptions);
|
|
204812
205055
|
if (lines.length > 0) {
|
|
204813
|
-
const hasVisionKey = !!(!skipVision && (process.env.OPENROUTER_API_KEY || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY));
|
|
204814
|
-
const header = hasVisionKey ? "# Asset Descriptions\n\nOne line per file. Read this instead of opening every image individually.\n\nTo find a specific brand or icon, **grep this file for the brand name in the description text** (e.g. `grep -i 'autodesk' asset-descriptions.md`). The Gemini Vision captions identify what's actually in each file \u2014 that's the agent's selector.\n\nThe `logo-<hash>.svg` filename prefix is a cheap structural hint (DOM said this SVG was inside a `<header>`, home-link `<a>`, or had an aria-label matching the page brand). It is NOT a content claim \u2014 many `logo-*` files are nav icons or decorative shapes. Trust the captions, not the filename prefix.\n\n" : "# Asset Descriptions\n\n\u26A0\uFE0F
|
|
205056
|
+
const hasVisionKey = !!(!skipVision && (process.env.OPENROUTER_API_KEY || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || process.env.HYPERFRAMES_VERTEX_PROJECT_ID && process.env.HYPERFRAMES_VERTEX_SERVICE_ACCOUNT));
|
|
205057
|
+
const header = hasVisionKey ? "# Asset Descriptions\n\nOne line per file. Read this instead of opening every image individually.\n\nTo find a specific brand or icon, **grep this file for the brand name in the description text** (e.g. `grep -i 'autodesk' asset-descriptions.md`). The Gemini Vision captions identify what's actually in each file \u2014 that's the agent's selector.\n\nThe `logo-<hash>.svg` filename prefix is a cheap structural hint (DOM said this SVG was inside a `<header>`, home-link `<a>`, or had an aria-label matching the page brand). It is NOT a content claim \u2014 many `logo-*` files are nav icons or decorative shapes. Trust the captions, not the filename prefix.\n\n" : "# Asset Descriptions\n\n\u26A0\uFE0F No vision credentials \u2014 descriptions below are catalog-derived (alt text, headings, section context, filename) instead of Vision-generated. To get richer Vision descriptions on the next capture, set GEMINI_API_KEY (or GOOGLE_API_KEY), or HYPERFRAMES_VERTEX_PROJECT_ID plus HYPERFRAMES_VERTEX_SERVICE_ACCOUNT for Vertex service-account auth, and re-run.\n\nThe `logo-<hash>.svg` filename prefix is a structural hint (DOM said this SVG was inside a `<header>`, home-link `<a>`, or had an aria-label matching the page brand). To pick the actual brand logo without Vision, open the `logo-*` candidates in a previewer or rasterize them with `sharp` before referencing \u2014 composing a fake logo ships off-brand in the final video.\n\n";
|
|
204815
205058
|
writeFileSync56(
|
|
204816
205059
|
join126(outputDir, "extracted", "asset-descriptions.md"),
|
|
204817
205060
|
header + lines.map((l) => "- " + l).join("\n") + "\n",
|