vidspotai-shared 1.0.94 → 1.0.96
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/lib/globals/aiModels/enums.d.ts +2 -0
- package/lib/globals/aiModels/enums.d.ts.map +1 -1
- package/lib/globals/aiModels/enums.js +5 -0
- package/lib/globals/aiModels/index.d.ts.map +1 -1
- package/lib/globals/aiModels/index.js +2 -0
- package/lib/globals/aiModels/providers/hostedAvatars.d.ts +22 -0
- package/lib/globals/aiModels/providers/hostedAvatars.d.ts.map +1 -0
- package/lib/globals/aiModels/providers/hostedAvatars.js +129 -0
- package/lib/globals/types.d.ts +58 -1
- package/lib/globals/types.d.ts.map +1 -1
- package/lib/globals/types.js +61 -1
- package/lib/models/demoVideoProject.model.d.ts +244 -0
- package/lib/models/demoVideoProject.model.d.ts.map +1 -0
- package/lib/models/demoVideoProject.model.js +2 -0
- package/lib/models/githubConnection.model.d.ts +35 -0
- package/lib/models/githubConnection.model.d.ts.map +1 -0
- package/lib/models/githubConnection.model.js +2 -0
- package/lib/models/index.d.ts +3 -0
- package/lib/models/index.d.ts.map +1 -1
- package/lib/models/index.js +3 -0
- package/lib/models/outreach.model.d.ts +175 -0
- package/lib/models/outreach.model.d.ts.map +1 -0
- package/lib/models/outreach.model.js +2 -0
- package/lib/models/video.model.d.ts +4 -0
- package/lib/models/video.model.d.ts.map +1 -1
- package/lib/schemas/brandKit.schema.d.ts +4 -0
- package/lib/schemas/brandKit.schema.d.ts.map +1 -1
- package/lib/schemas/brandKit.schema.js +10 -0
- package/lib/schemas/demoQuestion.schema.d.ts +286 -0
- package/lib/schemas/demoQuestion.schema.d.ts.map +1 -0
- package/lib/schemas/demoQuestion.schema.js +210 -0
- package/lib/schemas/demoSpec.schema.d.ts +109 -6
- package/lib/schemas/demoSpec.schema.d.ts.map +1 -1
- package/lib/schemas/demoSpec.schema.js +95 -7
- package/lib/schemas/index.d.ts +1 -0
- package/lib/schemas/index.d.ts.map +1 -1
- package/lib/schemas/index.js +1 -0
- package/lib/schemas/videoPlan.schema.d.ts +2 -2
- package/lib/schemas/videoPlan.schema.d.ts.map +1 -1
- package/lib/schemas/videoPlan.schema.js +11 -1
- package/lib/services/agent/providerFallback/chains.d.ts.map +1 -1
- package/lib/services/agent/providerFallback/chains.js +9 -1
- package/lib/services/agent/taskPoller.d.ts.map +1 -1
- package/lib/services/agent/taskPoller.js +17 -6
- package/lib/services/agent/tools/generateAvatarVideo.tool.d.ts +21 -9
- package/lib/services/agent/tools/generateAvatarVideo.tool.d.ts.map +1 -1
- package/lib/services/agent/tools/generateAvatarVideo.tool.js +135 -39
- package/lib/services/agent/tools/planVideo.tool.d.ts +2 -2
- package/lib/services/aiGen/aiGenFactory.service.d.ts +1 -1
- package/lib/services/aiGen/aiGenFactory.service.d.ts.map +1 -1
- package/lib/services/aiGen/aiGenFactory.service.js +10 -2
- package/lib/services/aiGen/helpers.d.ts.map +1 -1
- package/lib/services/aiGen/helpers.js +12 -2
- package/lib/services/aiGen/providers/kling/kling.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/kling/kling.service.js +47 -8
- package/lib/services/aiGen/providers/pixverse/pixverse.service.d.ts +8 -0
- package/lib/services/aiGen/providers/pixverse/pixverse.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/pixverse/pixverse.service.js +55 -2
- package/lib/services/avatarGen/IAvatarVideoProvider.d.ts +136 -0
- package/lib/services/avatarGen/IAvatarVideoProvider.d.ts.map +1 -0
- package/lib/services/avatarGen/IAvatarVideoProvider.js +42 -0
- package/lib/services/avatarGen/avatarProviderFactory.d.ts +18 -0
- package/lib/services/avatarGen/avatarProviderFactory.d.ts.map +1 -0
- package/lib/services/avatarGen/avatarProviderFactory.js +54 -0
- package/lib/services/avatarGen/index.d.ts +6 -0
- package/lib/services/avatarGen/index.d.ts.map +1 -0
- package/lib/services/avatarGen/index.js +21 -0
- package/lib/services/avatarGen/providers/heygenAvatarProvider.d.ts +45 -0
- package/lib/services/avatarGen/providers/heygenAvatarProvider.d.ts.map +1 -0
- package/lib/services/avatarGen/providers/heygenAvatarProvider.js +190 -0
- package/lib/services/avatarGen/providers/klingAvatarProvider.d.ts +32 -0
- package/lib/services/avatarGen/providers/klingAvatarProvider.d.ts.map +1 -0
- package/lib/services/avatarGen/providers/klingAvatarProvider.js +69 -0
- package/lib/services/avatarGen/providers/tavusAvatarProvider.d.ts +56 -0
- package/lib/services/avatarGen/providers/tavusAvatarProvider.d.ts.map +1 -0
- package/lib/services/avatarGen/providers/tavusAvatarProvider.js +183 -0
- package/lib/services/demo/demoAuthor.d.ts +45 -0
- package/lib/services/demo/demoAuthor.d.ts.map +1 -0
- package/lib/services/demo/demoAuthor.js +75 -0
- package/lib/services/demo/demoAuthorNormalize.d.ts +49 -0
- package/lib/services/demo/demoAuthorNormalize.d.ts.map +1 -0
- package/lib/services/demo/demoAuthorNormalize.js +327 -0
- package/lib/services/demo/demoAuthorPrompt.d.ts +102 -0
- package/lib/services/demo/demoAuthorPrompt.d.ts.map +1 -0
- package/lib/services/demo/demoAuthorPrompt.js +243 -0
- package/lib/services/demo/demoCredits.d.ts +20 -0
- package/lib/services/demo/demoCredits.d.ts.map +1 -0
- package/lib/services/demo/demoCredits.js +51 -0
- package/lib/services/demo/demoMediaGuard.d.ts +43 -0
- package/lib/services/demo/demoMediaGuard.d.ts.map +1 -0
- package/lib/services/demo/demoMediaGuard.js +207 -0
- package/lib/services/demo/demoMusic.d.ts +13 -0
- package/lib/services/demo/demoMusic.d.ts.map +1 -0
- package/lib/services/demo/demoMusic.js +93 -0
- package/lib/services/demo/demoPrefill.d.ts +76 -0
- package/lib/services/demo/demoPrefill.d.ts.map +1 -0
- package/lib/services/demo/demoPrefill.js +435 -0
- package/lib/services/demo/demoRevisePrompt.d.ts +17 -0
- package/lib/services/demo/demoRevisePrompt.d.ts.map +1 -0
- package/lib/services/demo/demoRevisePrompt.js +85 -0
- package/lib/services/demo/demoReviser.d.ts +63 -0
- package/lib/services/demo/demoReviser.d.ts.map +1 -0
- package/lib/services/demo/demoReviser.js +81 -0
- package/lib/services/demo/demoScriptPrompt.d.ts +63 -0
- package/lib/services/demo/demoScriptPrompt.d.ts.map +1 -0
- package/lib/services/demo/demoScriptPrompt.js +234 -0
- package/lib/services/demo/demoScriptWriter.d.ts +65 -0
- package/lib/services/demo/demoScriptWriter.d.ts.map +1 -0
- package/lib/services/demo/demoScriptWriter.js +211 -0
- package/lib/services/demo/demoTriage.d.ts +65 -0
- package/lib/services/demo/demoTriage.d.ts.map +1 -0
- package/lib/services/demo/demoTriage.js +130 -0
- package/lib/services/demo/githubOAuth.d.ts +78 -0
- package/lib/services/demo/githubOAuth.d.ts.map +1 -0
- package/lib/services/demo/githubOAuth.js +209 -0
- package/lib/services/demo/githubOAuthState.d.ts +10 -0
- package/lib/services/demo/githubOAuthState.d.ts.map +1 -0
- package/lib/services/demo/githubOAuthState.js +63 -0
- package/lib/services/demo/index.d.ts +24 -0
- package/lib/services/demo/index.d.ts.map +1 -0
- package/lib/services/demo/index.js +39 -0
- package/lib/services/firestore.service.d.ts +11 -0
- package/lib/services/firestore.service.d.ts.map +1 -1
- package/lib/services/firestore.service.js +24 -0
- package/lib/services/index.d.ts +2 -0
- package/lib/services/index.d.ts.map +1 -1
- package/lib/services/index.js +2 -0
- package/lib/services/tts/providers/elevenlabs.service.d.ts.map +1 -1
- package/lib/services/tts/providers/elevenlabs.service.js +16 -8
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.d.ts.map +1 -1
- package/lib/utils/index.js +1 -0
- package/lib/utils/outreachTemplate.d.ts +46 -0
- package/lib/utils/outreachTemplate.d.ts.map +1 -0
- package/lib/utils/outreachTemplate.js +174 -0
- package/package.json +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.selectDemoMusic = selectDemoMusic;
|
|
4
|
+
const musicGen_1 = require("../musicGen");
|
|
5
|
+
const logger_1 = require("../../utils/logger");
|
|
6
|
+
/**
|
|
7
|
+
* Background-music selection for the SaaS Demo Video pipeline (quality pillar #3:
|
|
8
|
+
* "exciting music, ducked under the voiceover"). Runs in the worker just before
|
|
9
|
+
* the mux stage — a purely mechanical style → mood/bpm → curated-track lookup, so
|
|
10
|
+
* it needs no LLM call (mirrors `selectMusicForPlan`'s post-planner rationale for
|
|
11
|
+
* the agent pipeline).
|
|
12
|
+
*
|
|
13
|
+
* Source is the always-on, zero-cost, commercially-licensed curated catalog
|
|
14
|
+
* (Pixabay Content License) via `searchAllMusicSources`. Product demos want
|
|
15
|
+
* ENERGY, so the mood floor is uplifting/energetic; `pace`/`motion` push the
|
|
16
|
+
* tempo up, and only a slow, dark, subtle style relaxes it. Best-effort: if no
|
|
17
|
+
* track is reachable we return null and the video ships without a bed (never a
|
|
18
|
+
* hard failure — music is enhancement, not a blocker).
|
|
19
|
+
*/
|
|
20
|
+
const TAG = "[DemoMusic]";
|
|
21
|
+
/**
|
|
22
|
+
* Map the resolved DemoStyle to a music mood + tempo target. The demo brief is
|
|
23
|
+
* always "an exciting product demo", so the baseline is upbeat; the levers only
|
|
24
|
+
* decide HOW driving. `colorMood:"dark"` biases toward a moodier-but-still-driving
|
|
25
|
+
* track when the pace is already relaxed, otherwise energy wins.
|
|
26
|
+
*/
|
|
27
|
+
function moodForStyle(spec) {
|
|
28
|
+
const { pace, motion, colorMood } = spec.style;
|
|
29
|
+
const highEnergy = pace === "fast" || motion === "dynamic";
|
|
30
|
+
const lowEnergy = pace === "slow" && motion === "subtle";
|
|
31
|
+
if (highEnergy) {
|
|
32
|
+
return { mood: "energetic", query: "upbeat energetic product demo", bpmTarget: 124 };
|
|
33
|
+
}
|
|
34
|
+
if (lowEnergy) {
|
|
35
|
+
// Relaxed pace — stay uplifting, or lean cinematic when the look is dark.
|
|
36
|
+
return colorMood === "dark"
|
|
37
|
+
? { mood: "cinematic", query: "cinematic driving product demo", bpmTarget: 96 }
|
|
38
|
+
: { mood: "uplifting", query: "warm uplifting product demo", bpmTarget: 100 };
|
|
39
|
+
}
|
|
40
|
+
// Balanced default — uplifting corporate/product-demo bed.
|
|
41
|
+
return { mood: "uplifting", query: "uplifting corporate product demo", bpmTarget: 112 };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* HEAD-probe a track URL to confirm it's fetchable before committing it — the
|
|
45
|
+
* curated catalog points at cdn.pixabay.com, alive today but worth guarding so
|
|
46
|
+
* a dead CDN edge silently ships no music rather than 404-crashing the mux.
|
|
47
|
+
* (Same guard shape as `selectMusicForPlan`'s `pickFirstReachable`.)
|
|
48
|
+
*/
|
|
49
|
+
async function firstReachable(tracks) {
|
|
50
|
+
for (const t of tracks) {
|
|
51
|
+
try {
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
54
|
+
const r = await fetch(t.url, { method: "HEAD", signal: controller.signal });
|
|
55
|
+
clearTimeout(timeout);
|
|
56
|
+
if (r.ok)
|
|
57
|
+
return t;
|
|
58
|
+
logger_1.logger.warn(`${TAG} track HEAD non-2xx, skipping`, { url: t.url, status: r.status });
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
logger_1.logger.warn(`${TAG} track HEAD failed, skipping`, {
|
|
62
|
+
url: t.url,
|
|
63
|
+
err: e instanceof Error ? e.message : String(e),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Pick a reachable background-music bed for a demo. Returns null (never throws)
|
|
71
|
+
* when nothing is reachable — the caller ships the video without a bed.
|
|
72
|
+
*/
|
|
73
|
+
async function selectDemoMusic(spec) {
|
|
74
|
+
const { mood, query, bpmTarget } = moodForStyle(spec);
|
|
75
|
+
try {
|
|
76
|
+
// Don't pass durationSecMin — every curated track is looped to the video
|
|
77
|
+
// length in the mux, so a shorter track must not be penalized/disqualified.
|
|
78
|
+
const result = await (0, musicGen_1.searchAllMusicSources)({ query, mood, bpmTarget, limit: 5 });
|
|
79
|
+
const track = await firstReachable(result.tracks);
|
|
80
|
+
if (!track) {
|
|
81
|
+
logger_1.logger.warn(`${TAG} no reachable track`, { mood, candidateCount: result.tracks.length });
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
logger_1.logger.info(`${TAG} selected bed`, { trackId: track.id, mood: track.mood, bpm: track.bpm });
|
|
85
|
+
return { url: track.url, attribution: track.attribution, mood: track.mood, bpm: track.bpm };
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
logger_1.logger.warn(`${TAG} selection failed — shipping without music`, {
|
|
89
|
+
err: e instanceof Error ? e.message : String(e),
|
|
90
|
+
});
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { LlmCaller } from "../agent/llmCaller";
|
|
2
|
+
import { ModelChoice, ModelRouter } from "../agent/modelRouter";
|
|
3
|
+
export interface DemoPrefillFields {
|
|
4
|
+
productName?: string;
|
|
5
|
+
tagline?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
audience?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface DemoPrefillResult {
|
|
10
|
+
fields: DemoPrefillFields;
|
|
11
|
+
/** How the fields were derived — "llm" polished, or metadata-only fallback. */
|
|
12
|
+
source: "llm" | "metadata";
|
|
13
|
+
modelUsed?: ModelChoice;
|
|
14
|
+
usage?: {
|
|
15
|
+
promptTokens?: number;
|
|
16
|
+
completionTokens?: number;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** User-facing error codes the controller maps to 4xx/5xx responses. */
|
|
20
|
+
export type DemoPrefillErrorCode = "invalid_url" | "blocked_host" | "unreachable" | "empty_page";
|
|
21
|
+
export declare class DemoPrefillError extends Error {
|
|
22
|
+
readonly code: DemoPrefillErrorCode;
|
|
23
|
+
constructor(code: DemoPrefillErrorCode, message: string);
|
|
24
|
+
}
|
|
25
|
+
export interface DemoPrefillDeps {
|
|
26
|
+
llm?: LlmCaller;
|
|
27
|
+
router?: ModelRouter;
|
|
28
|
+
/** Total fetch timeout per hop (ms). */
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
/** Max redirect hops to follow (each re-validated against the SSRF guard). */
|
|
31
|
+
maxRedirects?: number;
|
|
32
|
+
/** Max response bytes to read (defends against huge pages). */
|
|
33
|
+
maxBytes?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface DemoPageMeta {
|
|
36
|
+
title?: string;
|
|
37
|
+
siteName?: string;
|
|
38
|
+
ogTitle?: string;
|
|
39
|
+
ogDescription?: string;
|
|
40
|
+
metaDescription?: string;
|
|
41
|
+
jsonLdName?: string;
|
|
42
|
+
jsonLdDescription?: string;
|
|
43
|
+
/** De-tagged visible text, capped. */
|
|
44
|
+
text: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function extractPageMeta(html: string): DemoPageMeta;
|
|
47
|
+
/**
|
|
48
|
+
* Signals extracted from a product page for the WORKER's gather stage — the
|
|
49
|
+
* "background autofill" the user never has to click. Deliberately LLM-FREE and
|
|
50
|
+
* uncharged: it reuses the same SSRF-safe fetch + metadata extraction as the
|
|
51
|
+
* interactive prefill, but skips the Haiku polish so it's free to run on every
|
|
52
|
+
* generation. Used to backfill any Basics field the user left blank and to hand
|
|
53
|
+
* the planner a compact page summary as extra grounding.
|
|
54
|
+
*/
|
|
55
|
+
export interface DemoPageSignals {
|
|
56
|
+
/** Metadata-derived basics (only the fields we could determine). */
|
|
57
|
+
fields: DemoPrefillFields;
|
|
58
|
+
/** A compact page summary for the planner (title/desc + visible-text lead). */
|
|
59
|
+
summary: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Fetch a product URL and pull LLM-FREE signals for the gather stage: the
|
|
63
|
+
* metadata-only Basics + a compact summary string. Throws DemoPrefillError on
|
|
64
|
+
* the same user-facing failures as {@link prefillDemoBasics} (bad URL, blocked
|
|
65
|
+
* host, unreachable, empty page) — the worker calls this best-effort and
|
|
66
|
+
* swallows any throw, so a slow/blocked site never fails a generation.
|
|
67
|
+
*/
|
|
68
|
+
export declare function gatherPageSignals(rawUrl: string, deps?: Pick<DemoPrefillDeps, "timeoutMs" | "maxRedirects" | "maxBytes">): Promise<DemoPageSignals>;
|
|
69
|
+
/**
|
|
70
|
+
* Fetch a product URL and extract Basics fields. Tries LLM polish (Haiku "router"
|
|
71
|
+
* tier); falls back to metadata-only if the LLM is unconfigured/errors. Throws
|
|
72
|
+
* DemoPrefillError for user-facing failures (bad URL, blocked host, unreachable,
|
|
73
|
+
* empty page).
|
|
74
|
+
*/
|
|
75
|
+
export declare function prefillDemoBasics(rawUrl: string, deps?: DemoPrefillDeps): Promise<DemoPrefillResult>;
|
|
76
|
+
//# sourceMappingURL=demoPrefill.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"demoPrefill.d.ts","sourceRoot":"","sources":["../../../src/services/demo/demoPrefill.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AA0BhE,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,+EAA+E;IAC/E,MAAM,EAAE,KAAK,GAAG,UAAU,CAAC;IAC3B,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,KAAK,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9D;AAED,wEAAwE;AACxE,MAAM,MAAM,oBAAoB,GAC5B,aAAa,GACb,cAAc,GACd,aAAa,GACb,YAAY,CAAC;AAEjB,qBAAa,gBAAiB,SAAQ,KAAK;aAEvB,IAAI,EAAE,oBAAoB;gBAA1B,IAAI,EAAE,oBAAoB,EAC1C,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA6ID,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;CACd;AA+ED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAc1D;AA4ED;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,oEAAoE;IACpE,MAAM,EAAE,iBAAiB,CAAC;IAC1B,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC;CACjB;AAKD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,IAAI,CAAC,eAAe,EAAE,WAAW,GAAG,cAAc,GAAG,UAAU,CAAM,GAC1E,OAAO,CAAC,eAAe,CAAC,CAqC1B;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,eAAoB,GACzB,OAAO,CAAC,iBAAiB,CAAC,CA8C5B"}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DemoPrefillError = void 0;
|
|
4
|
+
exports.extractPageMeta = extractPageMeta;
|
|
5
|
+
exports.gatherPageSignals = gatherPageSignals;
|
|
6
|
+
exports.prefillDemoBasics = prefillDemoBasics;
|
|
7
|
+
const promises_1 = require("dns/promises");
|
|
8
|
+
const net_1 = require("net");
|
|
9
|
+
const zod_1 = require("zod");
|
|
10
|
+
const llmCallerRegistry_1 = require("../agent/llmCallerRegistry");
|
|
11
|
+
const modelRouter_1 = require("../agent/modelRouter");
|
|
12
|
+
/**
|
|
13
|
+
* SaaS Demo Video Generator — "Autofill from URL" (plan §9 / Basics step helper).
|
|
14
|
+
*
|
|
15
|
+
* A FAST, SYNCHRONOUS product-basics extractor for the wizard: given a product
|
|
16
|
+
* URL, fetch the page server-side (SSRF-guarded), parse its OpenGraph / meta /
|
|
17
|
+
* JSON-LD / visible text, then run ONE cheap Claude Haiku pass to turn that into
|
|
18
|
+
* clean, distinct fields (productName / tagline / description / audience). If the
|
|
19
|
+
* LLM call fails, we degrade to a metadata-only best-effort so the button still
|
|
20
|
+
* fills something.
|
|
21
|
+
*
|
|
22
|
+
* This is deliberately SEPARATE from the worker's heavy gather stage
|
|
23
|
+
* (`websiteCapture` puppeteer screenshots + `githubBrand`): those run async
|
|
24
|
+
* during generation and cost real time; this is a sub-second form helper. It is
|
|
25
|
+
* uncharged (a Haiku call over a few KB of text is negligible).
|
|
26
|
+
*/
|
|
27
|
+
// ── Field length caps (mirror the frontend wizard's maxLength on each Field) ──
|
|
28
|
+
const CAP = {
|
|
29
|
+
productName: 120,
|
|
30
|
+
tagline: 240,
|
|
31
|
+
description: 2000,
|
|
32
|
+
audience: 240,
|
|
33
|
+
};
|
|
34
|
+
class DemoPrefillError extends Error {
|
|
35
|
+
constructor(code, message) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.code = code;
|
|
38
|
+
this.name = "DemoPrefillError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.DemoPrefillError = DemoPrefillError;
|
|
42
|
+
// ── SSRF guard ───────────────────────────────────────────────────────────────
|
|
43
|
+
/** IPv4/IPv6 ranges we must never fetch (cloud metadata, loopback, LAN, etc.). */
|
|
44
|
+
function isPrivateAddress(ip) {
|
|
45
|
+
const v = (0, net_1.isIP)(ip);
|
|
46
|
+
if (v === 4) {
|
|
47
|
+
const p = ip.split(".").map(Number);
|
|
48
|
+
if (p.length !== 4 || p.some((n) => Number.isNaN(n)))
|
|
49
|
+
return true;
|
|
50
|
+
const a = p[0];
|
|
51
|
+
const b = p[1];
|
|
52
|
+
if (a === 10)
|
|
53
|
+
return true; // 10.0.0.0/8
|
|
54
|
+
if (a === 127)
|
|
55
|
+
return true; // loopback
|
|
56
|
+
if (a === 0)
|
|
57
|
+
return true; // "this" network
|
|
58
|
+
if (a === 169 && b === 254)
|
|
59
|
+
return true; // link-local incl. 169.254.169.254
|
|
60
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
61
|
+
return true; // 172.16.0.0/12
|
|
62
|
+
if (a === 192 && b === 168)
|
|
63
|
+
return true; // 192.168.0.0/16
|
|
64
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
65
|
+
return true; // CGNAT 100.64.0.0/10
|
|
66
|
+
if (a >= 224)
|
|
67
|
+
return true; // multicast / reserved
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (v === 6) {
|
|
71
|
+
const lower = ip.toLowerCase();
|
|
72
|
+
if (lower === "::1" || lower === "::")
|
|
73
|
+
return true; // loopback / unspecified
|
|
74
|
+
if (lower.startsWith("fc") || lower.startsWith("fd"))
|
|
75
|
+
return true; // ULA fc00::/7
|
|
76
|
+
if (lower.startsWith("fe80"))
|
|
77
|
+
return true; // link-local
|
|
78
|
+
// IPv4-mapped (::ffff:a.b.c.d) — extract and re-check as v4.
|
|
79
|
+
const mapped = lower.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
80
|
+
if (mapped?.[1])
|
|
81
|
+
return isPrivateAddress(mapped[1]);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return true; // not an IP literal ⇒ treat as unsafe
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Validate a URL is a public http(s) target: parseable, http/https, not a
|
|
88
|
+
* localhost-ish hostname, and every resolved IP is public. Throws a
|
|
89
|
+
* DemoPrefillError otherwise. Called on the initial URL AND every redirect hop.
|
|
90
|
+
*/
|
|
91
|
+
async function assertPublicHttpUrl(raw) {
|
|
92
|
+
let url;
|
|
93
|
+
try {
|
|
94
|
+
url = new URL(raw);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
throw new DemoPrefillError("invalid_url", "That doesn't look like a valid URL.");
|
|
98
|
+
}
|
|
99
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
100
|
+
throw new DemoPrefillError("invalid_url", "Only http(s) URLs are supported.");
|
|
101
|
+
}
|
|
102
|
+
const host = url.hostname.replace(/^\[|\]$/g, ""); // strip IPv6 brackets
|
|
103
|
+
// Obvious internal hostnames — reject before any DNS.
|
|
104
|
+
const lowered = host.toLowerCase();
|
|
105
|
+
if (lowered === "localhost" ||
|
|
106
|
+
lowered.endsWith(".localhost") ||
|
|
107
|
+
lowered.endsWith(".local") ||
|
|
108
|
+
lowered.endsWith(".internal") ||
|
|
109
|
+
lowered === "metadata.google.internal") {
|
|
110
|
+
throw new DemoPrefillError("blocked_host", "That host isn't reachable.");
|
|
111
|
+
}
|
|
112
|
+
// Literal IP ⇒ check directly; hostname ⇒ resolve every address and check all.
|
|
113
|
+
if ((0, net_1.isIP)(host)) {
|
|
114
|
+
if (isPrivateAddress(host)) {
|
|
115
|
+
throw new DemoPrefillError("blocked_host", "That host isn't reachable.");
|
|
116
|
+
}
|
|
117
|
+
return url;
|
|
118
|
+
}
|
|
119
|
+
let addresses;
|
|
120
|
+
try {
|
|
121
|
+
addresses = await (0, promises_1.lookup)(host, { all: true });
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
throw new DemoPrefillError("unreachable", "We couldn't reach that site.");
|
|
125
|
+
}
|
|
126
|
+
if (addresses.length === 0 || addresses.some((a) => isPrivateAddress(a.address))) {
|
|
127
|
+
throw new DemoPrefillError("blocked_host", "That host isn't reachable.");
|
|
128
|
+
}
|
|
129
|
+
return url;
|
|
130
|
+
}
|
|
131
|
+
// ── Fetch (SSRF-safe, manual redirects) ──────────────────────────────────────
|
|
132
|
+
async function safeFetchHtml(initialUrl, deps) {
|
|
133
|
+
let target = initialUrl;
|
|
134
|
+
for (let hop = 0; hop <= deps.maxRedirects; hop++) {
|
|
135
|
+
const url = await assertPublicHttpUrl(target);
|
|
136
|
+
const controller = new AbortController();
|
|
137
|
+
const timer = setTimeout(() => controller.abort(), deps.timeoutMs);
|
|
138
|
+
let res;
|
|
139
|
+
try {
|
|
140
|
+
res = await fetch(url.toString(), {
|
|
141
|
+
method: "GET",
|
|
142
|
+
redirect: "manual",
|
|
143
|
+
signal: controller.signal,
|
|
144
|
+
headers: {
|
|
145
|
+
// A real UA — some sites 403 headless clients; Accept HTML only.
|
|
146
|
+
"user-agent": "Mozilla/5.0 (compatible; VidSpotAIBot/1.0; +https://vidspotai.com/bot)",
|
|
147
|
+
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
throw new DemoPrefillError("unreachable", "We couldn't reach that site.");
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
}
|
|
157
|
+
// Manual redirect handling — re-validate the next hop against the guard.
|
|
158
|
+
if (res.status >= 300 && res.status < 400) {
|
|
159
|
+
const location = res.headers.get("location");
|
|
160
|
+
if (!location) {
|
|
161
|
+
throw new DemoPrefillError("unreachable", "That site redirected nowhere.");
|
|
162
|
+
}
|
|
163
|
+
target = new URL(location, url).toString();
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (!res.ok) {
|
|
167
|
+
throw new DemoPrefillError("unreachable", "That site returned an error.");
|
|
168
|
+
}
|
|
169
|
+
const ctype = res.headers.get("content-type") ?? "";
|
|
170
|
+
if (ctype && !/(text\/html|application\/xhtml|text\/plain|application\/xml)/i.test(ctype)) {
|
|
171
|
+
throw new DemoPrefillError("empty_page", "That link isn't a web page we can read.");
|
|
172
|
+
}
|
|
173
|
+
// Read at most maxBytes so a giant page can't exhaust memory.
|
|
174
|
+
const buf = await res.arrayBuffer();
|
|
175
|
+
const bytes = new Uint8Array(buf).subarray(0, deps.maxBytes);
|
|
176
|
+
return new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
177
|
+
}
|
|
178
|
+
throw new DemoPrefillError("unreachable", "That site redirected too many times.");
|
|
179
|
+
}
|
|
180
|
+
function decodeEntities(s) {
|
|
181
|
+
return s
|
|
182
|
+
.replace(/&/g, "&")
|
|
183
|
+
.replace(/</g, "<")
|
|
184
|
+
.replace(/>/g, ">")
|
|
185
|
+
.replace(/"/g, '"')
|
|
186
|
+
.replace(/�*39;|�*27;|'/gi, "'")
|
|
187
|
+
.replace(/ /g, " ")
|
|
188
|
+
.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)))
|
|
189
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, h) => String.fromCodePoint(parseInt(h, 16)));
|
|
190
|
+
}
|
|
191
|
+
function escapeRegex(s) {
|
|
192
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
193
|
+
}
|
|
194
|
+
/** Read a <meta property|name="key" content="..."> value, attribute-order agnostic. */
|
|
195
|
+
function metaContent(html, key) {
|
|
196
|
+
const k = escapeRegex(key);
|
|
197
|
+
const patterns = [
|
|
198
|
+
new RegExp(`<meta[^>]+(?:property|name)=["']${k}["'][^>]*content=["']([^"']*)["']`, "i"),
|
|
199
|
+
new RegExp(`<meta[^>]+content=["']([^"']*)["'][^>]*(?:property|name)=["']${k}["']`, "i"),
|
|
200
|
+
];
|
|
201
|
+
for (const re of patterns) {
|
|
202
|
+
const m = html.match(re);
|
|
203
|
+
const v = m?.[1]?.trim();
|
|
204
|
+
if (v)
|
|
205
|
+
return decodeEntities(v);
|
|
206
|
+
}
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
/** Best-effort JSON-LD name/description from the first Product/SoftwareApplication/Org node. */
|
|
210
|
+
function jsonLd(html) {
|
|
211
|
+
const blocks = html.match(/<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi);
|
|
212
|
+
if (!blocks)
|
|
213
|
+
return {};
|
|
214
|
+
for (const block of blocks) {
|
|
215
|
+
const body = block.replace(/^[\s\S]*?>/, "").replace(/<\/script>$/i, "");
|
|
216
|
+
let parsed;
|
|
217
|
+
try {
|
|
218
|
+
parsed = JSON.parse(body);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const nodes = Array.isArray(parsed)
|
|
224
|
+
? parsed
|
|
225
|
+
: parsed && typeof parsed === "object" && Array.isArray(parsed["@graph"])
|
|
226
|
+
? (parsed["@graph"])
|
|
227
|
+
: [parsed];
|
|
228
|
+
for (const node of nodes) {
|
|
229
|
+
if (!node || typeof node !== "object")
|
|
230
|
+
continue;
|
|
231
|
+
const obj = node;
|
|
232
|
+
const name = typeof obj.name === "string" ? obj.name.trim() : undefined;
|
|
233
|
+
const description = typeof obj.description === "string" ? obj.description.trim() : undefined;
|
|
234
|
+
if (name || description) {
|
|
235
|
+
return {
|
|
236
|
+
name: name ? decodeEntities(name) : undefined,
|
|
237
|
+
description: description ? decodeEntities(description) : undefined,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return {};
|
|
243
|
+
}
|
|
244
|
+
function extractVisibleText(html) {
|
|
245
|
+
const stripped = html
|
|
246
|
+
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
247
|
+
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
248
|
+
.replace(/<noscript[\s\S]*?<\/noscript>/gi, " ")
|
|
249
|
+
.replace(/<head[\s\S]*?<\/head>/gi, " ")
|
|
250
|
+
.replace(/<svg[\s\S]*?<\/svg>/gi, " ")
|
|
251
|
+
.replace(/<[^>]+>/g, " ");
|
|
252
|
+
return decodeEntities(stripped).replace(/\s+/g, " ").trim().slice(0, 4000);
|
|
253
|
+
}
|
|
254
|
+
function extractPageMeta(html) {
|
|
255
|
+
const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
256
|
+
const ld = jsonLd(html);
|
|
257
|
+
return {
|
|
258
|
+
title: titleMatch?.[1] ? decodeEntities(titleMatch[1].trim()) : undefined,
|
|
259
|
+
siteName: metaContent(html, "og:site_name"),
|
|
260
|
+
ogTitle: metaContent(html, "og:title") ?? metaContent(html, "twitter:title"),
|
|
261
|
+
ogDescription: metaContent(html, "og:description") ?? metaContent(html, "twitter:description"),
|
|
262
|
+
metaDescription: metaContent(html, "description"),
|
|
263
|
+
jsonLdName: ld.name,
|
|
264
|
+
jsonLdDescription: ld.description,
|
|
265
|
+
text: extractVisibleText(html),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
// ── Metadata-only fallback (no LLM) ──────────────────────────────────────────
|
|
269
|
+
function truncate(s, cap) {
|
|
270
|
+
if (!s)
|
|
271
|
+
return undefined;
|
|
272
|
+
const t = s.trim();
|
|
273
|
+
return t ? t.slice(0, cap) : undefined;
|
|
274
|
+
}
|
|
275
|
+
function firstSentence(s) {
|
|
276
|
+
if (!s)
|
|
277
|
+
return undefined;
|
|
278
|
+
const m = s.trim().match(/^.*?[.!?](?:\s|$)/);
|
|
279
|
+
return (m?.[0] ?? s).trim();
|
|
280
|
+
}
|
|
281
|
+
function metadataOnlyFields(meta) {
|
|
282
|
+
// Strip a common " — tagline" / " | Site" suffix off <title> for the name.
|
|
283
|
+
const rawName = meta.siteName ?? meta.jsonLdName ?? meta.ogTitle ?? meta.title;
|
|
284
|
+
const productName = rawName?.split(/\s+[|–—\-]\s+/)[0];
|
|
285
|
+
const desc = meta.metaDescription ?? meta.ogDescription ?? meta.jsonLdDescription;
|
|
286
|
+
return {
|
|
287
|
+
productName: truncate(productName, CAP.productName),
|
|
288
|
+
tagline: truncate(firstSentence(desc), CAP.tagline),
|
|
289
|
+
description: truncate(desc, CAP.description),
|
|
290
|
+
audience: undefined, // rarely in metadata — LLM fills this
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
// ── LLM polish ───────────────────────────────────────────────────────────────
|
|
294
|
+
const PolishSchema = zod_1.z.object({
|
|
295
|
+
productName: zod_1.z.string().default(""),
|
|
296
|
+
tagline: zod_1.z.string().default(""),
|
|
297
|
+
description: zod_1.z.string().default(""),
|
|
298
|
+
audience: zod_1.z.string().default(""),
|
|
299
|
+
});
|
|
300
|
+
function buildPolishSystem() {
|
|
301
|
+
return [
|
|
302
|
+
"You extract product-marketing basics from a company/product web page.",
|
|
303
|
+
"Return four concise fields, grounded ONLY in the provided page content — never invent facts.",
|
|
304
|
+
"",
|
|
305
|
+
"- productName: the product or brand name (no tagline, no domain).",
|
|
306
|
+
"- tagline: ONE short value-proposition line, ≤12 words, no trailing period.",
|
|
307
|
+
"- description: 1–3 plain sentences on what the product does and its main value.",
|
|
308
|
+
"- audience: who it's for (role/industry/company-type), if discernible.",
|
|
309
|
+
"",
|
|
310
|
+
"If a field can't be determined from the content, return an empty string for it.",
|
|
311
|
+
"Prefer the site's own words. No marketing fluff you can't support from the page.",
|
|
312
|
+
].join("\n");
|
|
313
|
+
}
|
|
314
|
+
function buildPolishUser(url, meta) {
|
|
315
|
+
const lines = [`URL: ${url}`];
|
|
316
|
+
if (meta.siteName)
|
|
317
|
+
lines.push(`og:site_name: ${meta.siteName}`);
|
|
318
|
+
if (meta.title)
|
|
319
|
+
lines.push(`<title>: ${meta.title}`);
|
|
320
|
+
if (meta.ogTitle)
|
|
321
|
+
lines.push(`og:title: ${meta.ogTitle}`);
|
|
322
|
+
if (meta.ogDescription)
|
|
323
|
+
lines.push(`og:description: ${meta.ogDescription}`);
|
|
324
|
+
if (meta.metaDescription)
|
|
325
|
+
lines.push(`meta description: ${meta.metaDescription}`);
|
|
326
|
+
if (meta.jsonLdName)
|
|
327
|
+
lines.push(`schema.org name: ${meta.jsonLdName}`);
|
|
328
|
+
if (meta.jsonLdDescription)
|
|
329
|
+
lines.push(`schema.org description: ${meta.jsonLdDescription}`);
|
|
330
|
+
if (meta.text)
|
|
331
|
+
lines.push("", "Visible page text (truncated):", meta.text);
|
|
332
|
+
return lines.join("\n");
|
|
333
|
+
}
|
|
334
|
+
/** Merge LLM output over the metadata fallback (LLM wins per-field when non-empty). */
|
|
335
|
+
function mergeFields(llm, fallback) {
|
|
336
|
+
return {
|
|
337
|
+
productName: truncate(llm.productName || fallback.productName, CAP.productName),
|
|
338
|
+
tagline: truncate(llm.tagline || fallback.tagline, CAP.tagline),
|
|
339
|
+
description: truncate(llm.description || fallback.description, CAP.description),
|
|
340
|
+
audience: truncate(llm.audience || fallback.audience, CAP.audience),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/** Cap the planner-facing summary so it never balloons vision/text token cost. */
|
|
344
|
+
const SUMMARY_CAP = 1200;
|
|
345
|
+
/**
|
|
346
|
+
* Fetch a product URL and pull LLM-FREE signals for the gather stage: the
|
|
347
|
+
* metadata-only Basics + a compact summary string. Throws DemoPrefillError on
|
|
348
|
+
* the same user-facing failures as {@link prefillDemoBasics} (bad URL, blocked
|
|
349
|
+
* host, unreachable, empty page) — the worker calls this best-effort and
|
|
350
|
+
* swallows any throw, so a slow/blocked site never fails a generation.
|
|
351
|
+
*/
|
|
352
|
+
async function gatherPageSignals(rawUrl, deps = {}) {
|
|
353
|
+
const html = await safeFetchHtml(rawUrl, {
|
|
354
|
+
timeoutMs: deps.timeoutMs ?? 8000,
|
|
355
|
+
maxRedirects: deps.maxRedirects ?? 4,
|
|
356
|
+
maxBytes: deps.maxBytes ?? 1500000,
|
|
357
|
+
});
|
|
358
|
+
const meta = extractPageMeta(html);
|
|
359
|
+
const hasSignal = Boolean(meta.title || meta.ogTitle || meta.metaDescription || meta.ogDescription) ||
|
|
360
|
+
meta.text.length > 40;
|
|
361
|
+
if (!hasSignal) {
|
|
362
|
+
throw new DemoPrefillError("empty_page", "We couldn't read anything useful from that page.");
|
|
363
|
+
}
|
|
364
|
+
// Compose a compact planner summary from the strongest signals, de-duplicated.
|
|
365
|
+
const parts = [
|
|
366
|
+
meta.ogTitle ?? meta.title,
|
|
367
|
+
meta.ogDescription ?? meta.metaDescription ?? meta.jsonLdDescription,
|
|
368
|
+
meta.text,
|
|
369
|
+
].filter((p) => Boolean(p && p.trim()));
|
|
370
|
+
const seen = new Set();
|
|
371
|
+
const summary = parts
|
|
372
|
+
.filter((p) => {
|
|
373
|
+
const key = p.trim().toLowerCase();
|
|
374
|
+
if (seen.has(key))
|
|
375
|
+
return false;
|
|
376
|
+
seen.add(key);
|
|
377
|
+
return true;
|
|
378
|
+
})
|
|
379
|
+
.join(" — ")
|
|
380
|
+
.slice(0, SUMMARY_CAP)
|
|
381
|
+
.trim();
|
|
382
|
+
return {
|
|
383
|
+
fields: metadataOnlyFields(meta),
|
|
384
|
+
summary,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Fetch a product URL and extract Basics fields. Tries LLM polish (Haiku "router"
|
|
389
|
+
* tier); falls back to metadata-only if the LLM is unconfigured/errors. Throws
|
|
390
|
+
* DemoPrefillError for user-facing failures (bad URL, blocked host, unreachable,
|
|
391
|
+
* empty page).
|
|
392
|
+
*/
|
|
393
|
+
async function prefillDemoBasics(rawUrl, deps = {}) {
|
|
394
|
+
const html = await safeFetchHtml(rawUrl, {
|
|
395
|
+
timeoutMs: deps.timeoutMs ?? 8000,
|
|
396
|
+
maxRedirects: deps.maxRedirects ?? 4,
|
|
397
|
+
maxBytes: deps.maxBytes ?? 1500000,
|
|
398
|
+
});
|
|
399
|
+
const meta = extractPageMeta(html);
|
|
400
|
+
const fallback = metadataOnlyFields(meta);
|
|
401
|
+
const hasSignal = Boolean(meta.title || meta.ogTitle || meta.metaDescription || meta.ogDescription) ||
|
|
402
|
+
meta.text.length > 40;
|
|
403
|
+
if (!hasSignal) {
|
|
404
|
+
throw new DemoPrefillError("empty_page", "We couldn't read anything useful from that page.");
|
|
405
|
+
}
|
|
406
|
+
const llm = deps.llm ?? (0, llmCallerRegistry_1.getLlmCaller)();
|
|
407
|
+
const router = deps.router ?? new modelRouter_1.ModelRouter();
|
|
408
|
+
const model = router.pickFor("router");
|
|
409
|
+
const fallbackModel = router.fallbackFor("router");
|
|
410
|
+
try {
|
|
411
|
+
const result = await llm.structured({
|
|
412
|
+
model,
|
|
413
|
+
fallbackModel,
|
|
414
|
+
messages: [
|
|
415
|
+
{ role: "system", content: buildPolishSystem() },
|
|
416
|
+
{ role: "user", content: buildPolishUser(rawUrl, meta) },
|
|
417
|
+
],
|
|
418
|
+
schema: PolishSchema,
|
|
419
|
+
schemaName: "DemoBasics",
|
|
420
|
+
temperature: 0.2,
|
|
421
|
+
maxTokens: 500,
|
|
422
|
+
});
|
|
423
|
+
const polished = PolishSchema.parse(result.data);
|
|
424
|
+
return {
|
|
425
|
+
fields: mergeFields(polished, fallback),
|
|
426
|
+
source: "llm",
|
|
427
|
+
modelUsed: result.usedFallback && fallbackModel ? fallbackModel : model,
|
|
428
|
+
usage: result.usage,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
// LLM unconfigured / rate-limited / malformed → still return metadata.
|
|
433
|
+
return { fields: fallback, source: "metadata" };
|
|
434
|
+
}
|
|
435
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DemoSpec } from "../../schemas/demoSpec.schema";
|
|
2
|
+
import { DemoAuthorPromptOpts } from "./demoAuthorPrompt";
|
|
3
|
+
/**
|
|
4
|
+
* Build the reviser system prompt: the full author prompt (vocabulary + rules +
|
|
5
|
+
* quality bar) plus the REVISION MODE preamble. `allowCustomScenes` threads the
|
|
6
|
+
* worker's `DEMO_CODEGEN_ENABLED` gate exactly as the author does, so a revision
|
|
7
|
+
* can only introduce a `custom` scene when the codegen agent that fills it is on.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildDemoReviseSystemPrompt(opts?: DemoAuthorPromptOpts): string;
|
|
10
|
+
/**
|
|
11
|
+
* Build the reviser user turn: the current spec (as JSON) + the enumerated
|
|
12
|
+
* citable images (product screenshots + the user's per-edit reference images) +
|
|
13
|
+
* the user's natural-language change request. Images are attached separately as
|
|
14
|
+
* `imageUrls` (mirrors the author).
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildDemoReviseUserText(spec: DemoSpec, instruction: string, productScreenshotUrls: string[] | undefined, referenceImageUrls: string[] | undefined): string;
|
|
17
|
+
//# sourceMappingURL=demoRevisePrompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"demoRevisePrompt.d.ts","sourceRoot":"","sources":["../../../src/services/demo/demoRevisePrompt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AACzD,OAAO,EACL,oBAAoB,EAErB,MAAM,oBAAoB,CAAC;AA6B5B;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,GAAE,oBAAyB,GAC9B,MAAM,CAER;AAqCD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,QAAQ,EACd,WAAW,EAAE,MAAM,EACnB,qBAAqB,EAAE,MAAM,EAAE,GAAG,SAAS,EAC3C,kBAAkB,EAAE,MAAM,EAAE,GAAG,SAAS,GACvC,MAAM,CAYR"}
|