vidspotai-shared 1.0.95 → 1.0.97
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 +3 -0
- package/lib/globals/aiModels/enums.d.ts.map +1 -1
- package/lib/globals/aiModels/enums.js +9 -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/aiModels/providers/kling.d.ts.map +1 -1
- package/lib/globals/aiModels/providers/kling.js +85 -1
- package/lib/globals/creditUnit.d.ts +29 -0
- package/lib/globals/creditUnit.d.ts.map +1 -1
- package/lib/globals/creditUnit.js +30 -1
- 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/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/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 +11 -2
- package/lib/services/aiGen/providers/kling/kling.service.d.ts +36 -1
- package/lib/services/aiGen/providers/kling/kling.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/kling/kling.service.js +62 -5
- package/lib/services/aiGen/providers/kling/klingCredits.d.ts.map +1 -1
- package/lib/services/aiGen/providers/kling/klingCredits.js +12 -0
- package/lib/services/aiGen/providers/kling/klingRequests.d.ts +14 -1
- package/lib/services/aiGen/providers/kling/klingRequests.d.ts.map +1 -1
- package/lib/services/aiGen/providers/kling/klingRequests.js +44 -0
- package/lib/services/aiGen/providers/kling/klingStatus.d.ts +13 -1
- package/lib/services/aiGen/providers/kling/klingStatus.d.ts.map +1 -1
- package/lib/services/aiGen/providers/kling/klingStatus.js +111 -0
- package/lib/services/aiGen/providers/kling/types.d.ts +71 -0
- package/lib/services/aiGen/providers/kling/types.d.ts.map +1 -1
- 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,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.writeDemoScript = writeDemoScript;
|
|
4
|
+
exports.normalizeDemoScript = normalizeDemoScript;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const llmCallerRegistry_1 = require("../agent/llmCallerRegistry");
|
|
7
|
+
const modelRouter_1 = require("../agent/modelRouter");
|
|
8
|
+
const demoScriptPrompt_1 = require("./demoScriptPrompt");
|
|
9
|
+
/**
|
|
10
|
+
* DemoSpec narration writer — the "script" stage of the SaaS Demo Video pipeline
|
|
11
|
+
* (plan Phase 5 / §4 stage 4 / §6).
|
|
12
|
+
*
|
|
13
|
+
* ONE Claude `.structured()` call turns the already-rendered DemoSpec timeline
|
|
14
|
+
* into a per-scene, timeline-synced narration script. Because the scene durations
|
|
15
|
+
* are fixed by the time this runs, every segment's `startMs` is deterministic
|
|
16
|
+
* (cumulative scene offset) and its window is the scene's own duration — no
|
|
17
|
+
* recording analysis, no timestamp guessing. The voice stage (worker) then
|
|
18
|
+
* TTS-synthesises each segment and speed-fits it to the window.
|
|
19
|
+
*
|
|
20
|
+
* Mirrors `authorDemoSpec`: provider-agnostic (injected `LlmCaller`),
|
|
21
|
+
* model-routed, hardened by a deterministic normalize pass that enforces the
|
|
22
|
+
* invariants the prompt can only request (valid sceneIds, one segment per scene,
|
|
23
|
+
* timeline order + offsets).
|
|
24
|
+
*/
|
|
25
|
+
/** LLM output contract — the model returns raw per-scene lines; we compute timing. */
|
|
26
|
+
const DemoScriptDraftSchema = zod_1.z.object({
|
|
27
|
+
segments: zod_1.z
|
|
28
|
+
.array(zod_1.z.object({
|
|
29
|
+
/** Must match a DemoScene.id from the spec. */
|
|
30
|
+
sceneId: zod_1.z.string(),
|
|
31
|
+
/** Spoken narration for the scene (empty ⇒ scene left silent). */
|
|
32
|
+
text: zod_1.z.string(),
|
|
33
|
+
}))
|
|
34
|
+
.default([]),
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* Write a timeline-synced narration script for a rendered DemoSpec. Pure
|
|
38
|
+
* orchestration: build the per-scene briefs, call `.structured()`, then normalize
|
|
39
|
+
* to concrete `IDemoVideoScriptSegment[]` (compute offsets, drop unknown/empty
|
|
40
|
+
* scenes, dedupe, order). Returns an empty `segments` list (not a throw) when the
|
|
41
|
+
* model narrates nothing — the caller decides whether that means "silent final".
|
|
42
|
+
*/
|
|
43
|
+
async function writeDemoScript(spec, deps = {}) {
|
|
44
|
+
const llm = deps.llm ?? (0, llmCallerRegistry_1.getLlmCaller)();
|
|
45
|
+
const router = deps.router ?? new modelRouter_1.ModelRouter();
|
|
46
|
+
const model = router.pickFor("planner", { premium: deps.premium });
|
|
47
|
+
const fallbackModel = router.fallbackFor("planner");
|
|
48
|
+
const briefs = (0, demoScriptPrompt_1.buildDemoScriptBriefs)(spec);
|
|
49
|
+
const system = (0, demoScriptPrompt_1.buildDemoScriptSystemPrompt)();
|
|
50
|
+
const userText = (0, demoScriptPrompt_1.buildDemoScriptUserText)(spec, briefs);
|
|
51
|
+
const result = await llm.structured({
|
|
52
|
+
model,
|
|
53
|
+
fallbackModel,
|
|
54
|
+
messages: [
|
|
55
|
+
{ role: "system", content: system },
|
|
56
|
+
{ role: "user", content: userText },
|
|
57
|
+
],
|
|
58
|
+
schema: DemoScriptDraftSchema,
|
|
59
|
+
schemaName: "DemoScript",
|
|
60
|
+
temperature: deps.temperature ?? 0.6,
|
|
61
|
+
maxTokens: deps.maxTokens ?? 2000,
|
|
62
|
+
});
|
|
63
|
+
const draft = DemoScriptDraftSchema.parse(result.data);
|
|
64
|
+
let { segments, adjustments } = normalizeDemoScript(spec, draft.segments);
|
|
65
|
+
let usedHintFallback = false;
|
|
66
|
+
// Robustness: the dedicated writer has been observed to return ZERO usable
|
|
67
|
+
// segments (empty array, or ids that all drop) even when the author gave a
|
|
68
|
+
// per-scene `narrationHint`. For a voiceover/captions video that silently
|
|
69
|
+
// ships a SILENT final (the worker skips voice/mux when segments is empty) —
|
|
70
|
+
// a whiff on one LLM call shouldn't strip a video's intended narration. Fall
|
|
71
|
+
// back to the authored hints (already scene-appropriate spoken intent) run
|
|
72
|
+
// through the same normalize path. Music-led videos are left silent as before.
|
|
73
|
+
if (segments.length === 0 && spec.style.audioIntent !== "music") {
|
|
74
|
+
const hintSegments = spec.scenes
|
|
75
|
+
.filter((s) => (s.narrationHint ?? "").trim().length > 0)
|
|
76
|
+
.map((s) => ({ sceneId: s.id, text: s.narrationHint.trim() }));
|
|
77
|
+
if (hintSegments.length > 0) {
|
|
78
|
+
const fromHints = normalizeDemoScript(spec, hintSegments);
|
|
79
|
+
segments = fromHints.segments;
|
|
80
|
+
adjustments = [
|
|
81
|
+
...adjustments,
|
|
82
|
+
`script writer returned no usable segments — fell back to ${segments.length} authored narrationHint(s)`,
|
|
83
|
+
...fromHints.adjustments,
|
|
84
|
+
];
|
|
85
|
+
usedHintFallback = segments.length > 0;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
segments,
|
|
90
|
+
adjustments,
|
|
91
|
+
modelUsed: result.usedFallback && fallbackModel ? fallbackModel : model,
|
|
92
|
+
usedFallback: result.usedFallback,
|
|
93
|
+
usedHintFallback,
|
|
94
|
+
usage: result.usage,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** Lowercase + strip non-alphanumerics — collapses id/type spelling drift. */
|
|
98
|
+
function normalizeIdKey(value) {
|
|
99
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Deterministic guarantees the prompt can only request:
|
|
103
|
+
* - resolve each segment to a REAL scene (drop truly unknown sceneIds),
|
|
104
|
+
* - one segment per scene (keep the first; drop later duplicates),
|
|
105
|
+
* - drop blank narration,
|
|
106
|
+
* - stamp `startMs` (cumulative scene offset) + `durationMs` (the scene window;
|
|
107
|
+
* the voice stage overwrites `durationMs` with the actual spoken length),
|
|
108
|
+
* - emit in timeline order.
|
|
109
|
+
*
|
|
110
|
+
* `sceneId` resolution is tolerant by design. The script LLM is a separate call
|
|
111
|
+
* from the planner and has been observed to echo the scene *type* (`logoReveal`,
|
|
112
|
+
* `problem`, …) or a punctuation-drifted slug instead of the planner's exact
|
|
113
|
+
* `scene.id`. Rather than drop every segment (which silently ships a captionless,
|
|
114
|
+
* voiceover-less video), we resolve a raw id in three tiers: exact id → id with
|
|
115
|
+
* spelling drift normalized → the scene *type* when that type occurs exactly once
|
|
116
|
+
* in the timeline (so the mapping is unambiguous). Only a raw id that matches
|
|
117
|
+
* none of these is dropped. Each non-exact resolution is recorded as an
|
|
118
|
+
* adjustment for observability.
|
|
119
|
+
*/
|
|
120
|
+
function normalizeDemoScript(spec, rawSegments) {
|
|
121
|
+
const adjustments = [];
|
|
122
|
+
// Scene id → { startMs, durationMs, order } by walking the timeline once, plus
|
|
123
|
+
// fallback lookup tables (normalized id, and type→id only when the type is
|
|
124
|
+
// unique so the resolution is unambiguous).
|
|
125
|
+
const sceneWindows = new Map();
|
|
126
|
+
const byNormalizedId = new Map();
|
|
127
|
+
const typeCount = new Map();
|
|
128
|
+
const firstIdOfType = new Map();
|
|
129
|
+
let offset = 0;
|
|
130
|
+
spec.scenes.forEach((scene, order) => {
|
|
131
|
+
sceneWindows.set(scene.id, { startMs: offset, durationMs: scene.durationMs, order });
|
|
132
|
+
offset += scene.durationMs;
|
|
133
|
+
byNormalizedId.set(normalizeIdKey(scene.id), scene.id);
|
|
134
|
+
typeCount.set(scene.type, (typeCount.get(scene.type) ?? 0) + 1);
|
|
135
|
+
if (!firstIdOfType.has(scene.type))
|
|
136
|
+
firstIdOfType.set(scene.type, scene.id);
|
|
137
|
+
});
|
|
138
|
+
// type (and normalized type) → id, only for types that appear exactly once.
|
|
139
|
+
const uniqueTypeToId = new Map();
|
|
140
|
+
for (const [type, count] of typeCount) {
|
|
141
|
+
if (count !== 1)
|
|
142
|
+
continue;
|
|
143
|
+
const id = firstIdOfType.get(type);
|
|
144
|
+
if (!id)
|
|
145
|
+
continue;
|
|
146
|
+
uniqueTypeToId.set(type, id);
|
|
147
|
+
uniqueTypeToId.set(normalizeIdKey(type), id);
|
|
148
|
+
}
|
|
149
|
+
/** Resolve a raw sceneId to a real scene id, or null if unresolvable. */
|
|
150
|
+
const resolve = (rawId) => {
|
|
151
|
+
if (sceneWindows.has(rawId))
|
|
152
|
+
return { id: rawId };
|
|
153
|
+
const key = normalizeIdKey(rawId);
|
|
154
|
+
const byId = byNormalizedId.get(key);
|
|
155
|
+
if (byId && byId !== rawId) {
|
|
156
|
+
return { id: byId, note: `resolved sceneId "${rawId}" → "${byId}" (spelling drift)` };
|
|
157
|
+
}
|
|
158
|
+
const byType = uniqueTypeToId.get(rawId) ?? uniqueTypeToId.get(key);
|
|
159
|
+
if (byType) {
|
|
160
|
+
return { id: byType, note: `resolved sceneId "${rawId}" → "${byType}" (matched unique scene type)` };
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
};
|
|
164
|
+
const seen = new Set();
|
|
165
|
+
const segments = [];
|
|
166
|
+
for (const raw of rawSegments) {
|
|
167
|
+
const text = (raw.text ?? "").trim();
|
|
168
|
+
const resolved = resolve(raw.sceneId);
|
|
169
|
+
if (!resolved) {
|
|
170
|
+
adjustments.push(`dropped segment for unknown sceneId "${raw.sceneId}"`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const window = sceneWindows.get(resolved.id);
|
|
174
|
+
if (!window) {
|
|
175
|
+
// Unreachable (resolve only returns known ids) but keeps the type honest.
|
|
176
|
+
adjustments.push(`dropped segment for unknown sceneId "${raw.sceneId}"`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (!text) {
|
|
180
|
+
// An explicit empty line = "leave this scene silent"; not an error.
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (seen.has(resolved.id)) {
|
|
184
|
+
adjustments.push(`dropped duplicate segment for sceneId "${resolved.id}"`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
seen.add(resolved.id);
|
|
188
|
+
if (resolved.note)
|
|
189
|
+
adjustments.push(resolved.note);
|
|
190
|
+
// Hard-enforce the spoken-word budget the prompt only *asked* for: an
|
|
191
|
+
// over-budget line (the writer overshoots ~2×) is deterministically trimmed
|
|
192
|
+
// to fit the scene window, so the synthesized voice never overruns into the
|
|
193
|
+
// next scene (the overlapping-voices defect) or gets clipped/rushed by the
|
|
194
|
+
// voice stage. This is the correct-by-construction half of the no-overlap
|
|
195
|
+
// guarantee — the voice-stage atempo net now only ever handles minor residue.
|
|
196
|
+
const budget = (0, demoScriptPrompt_1.scriptWordBudget)(window.durationMs);
|
|
197
|
+
const fitted = (0, demoScriptPrompt_1.fitNarrationToBudget)(text, budget);
|
|
198
|
+
if (fitted.trimmed) {
|
|
199
|
+
adjustments.push(`trimmed narration for "${resolved.id}" to fit its ${Math.round(window.durationMs / 1000)}s window (${(0, demoScriptPrompt_1.wordCount)(text)}→${(0, demoScriptPrompt_1.wordCount)(fitted.text)} words, budget ${budget})`);
|
|
200
|
+
}
|
|
201
|
+
segments.push({
|
|
202
|
+
sceneId: resolved.id,
|
|
203
|
+
text: fitted.text,
|
|
204
|
+
startMs: window.startMs,
|
|
205
|
+
durationMs: window.durationMs,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
// Timeline order — the model is asked for order but we don't rely on it.
|
|
209
|
+
segments.sort((a, b) => (sceneWindows.get(a.sceneId).order - sceneWindows.get(b.sceneId).order));
|
|
210
|
+
return { segments, adjustments };
|
|
211
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { LlmCaller } from "../agent/llmCaller";
|
|
2
|
+
import { ModelChoice, ModelRouter } from "../agent/modelRouter";
|
|
3
|
+
import { DemoTriageResult } from "../../schemas/demoQuestion.schema";
|
|
4
|
+
/**
|
|
5
|
+
* SaaS Demo Video Generator — DYNAMIC clarification triage (plan §clarification).
|
|
6
|
+
*
|
|
7
|
+
* The wizard's fixed §2.6 style/pace questions cover the GENERIC axes. This adds
|
|
8
|
+
* the PRODUCT-SPECIFIC layer the user asked for: after the initial read of the
|
|
9
|
+
* website + the details they gave, a cheap Haiku pass decides whether a few
|
|
10
|
+
* targeted questions would MATERIALLY improve the video — and, only if so, what to
|
|
11
|
+
* ask. A clear, complete brief returns `ask:false` (no questions, just generate).
|
|
12
|
+
*
|
|
13
|
+
* ONE contract (`DemoQuestion`) powers BOTH surfaces:
|
|
14
|
+
* - PRE-GEN: the /triage endpoint calls this before any spend; the wizard renders
|
|
15
|
+
* the questions inline; answers ride the create call as `clarifyAnswers`.
|
|
16
|
+
* - MID-RUN: the worker can call this at an ambiguity point and pause.
|
|
17
|
+
*
|
|
18
|
+
* Fail-soft everywhere: any error / unconfigured LLM ⇒ `{ ask:false }`, so
|
|
19
|
+
* clarification never blocks generation. Uncharged (a Haiku call over a short
|
|
20
|
+
* brief is negligible, exactly like demoPrefill).
|
|
21
|
+
*/
|
|
22
|
+
/** What we know about the product at triage time (from the wizard + autofill). */
|
|
23
|
+
export interface DemoTriageContext {
|
|
24
|
+
productName: string;
|
|
25
|
+
tagline?: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
audience?: string;
|
|
28
|
+
websiteUrl?: string;
|
|
29
|
+
/** Presence of each input source — lets triage ask to fill a gap. */
|
|
30
|
+
hasWebsite: boolean;
|
|
31
|
+
hasGithub: boolean;
|
|
32
|
+
hasScreenshots: boolean;
|
|
33
|
+
/** Featured media the user attached (drives "how should we use this clip?" questions). */
|
|
34
|
+
mediaAssets?: Array<{
|
|
35
|
+
kind: "image" | "video";
|
|
36
|
+
label?: string;
|
|
37
|
+
}>;
|
|
38
|
+
/** Features the user already listed to highlight. */
|
|
39
|
+
features?: string[];
|
|
40
|
+
/**
|
|
41
|
+
* Which fixed §2.6 axes the user ALREADY pinned (true = pinned, not "decide for
|
|
42
|
+
* me"). Triage must NOT re-ask a pinned axis — it complements, never duplicates,
|
|
43
|
+
* the static wizard.
|
|
44
|
+
*/
|
|
45
|
+
pinned?: Partial<Record<"visualStyle" | "pace" | "targetLength" | "narrative" | "aspect" | "colorMood" | "audioIntent", boolean>>;
|
|
46
|
+
/** Optional extra page/summary signal (e.g. from the gather stage) to ground questions. */
|
|
47
|
+
pageSummary?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface DemoTriageDeps {
|
|
50
|
+
llm?: LlmCaller;
|
|
51
|
+
router?: ModelRouter;
|
|
52
|
+
}
|
|
53
|
+
export interface DemoTriageOutcome extends DemoTriageResult {
|
|
54
|
+
modelUsed?: ModelChoice;
|
|
55
|
+
usage?: {
|
|
56
|
+
promptTokens?: number;
|
|
57
|
+
completionTokens?: number;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Run the triage pass. Returns `{ ask, reason, questions }`. Never throws — an
|
|
62
|
+
* unconfigured/erroring LLM degrades to `{ ask:false }` so generation proceeds.
|
|
63
|
+
*/
|
|
64
|
+
export declare function triageDemoClarifications(ctx: DemoTriageContext, deps?: DemoTriageDeps): Promise<DemoTriageOutcome>;
|
|
65
|
+
//# sourceMappingURL=demoTriage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"demoTriage.d.ts","sourceRoot":"","sources":["../../../src/services/demo/demoTriage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,EAEL,gBAAgB,EAEjB,MAAM,mCAAmC,CAAC;AAE3C;;;;;;;;;;;;;;;;;GAiBG;AAEH,kFAAkF;AAClF,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,OAAO,CAAC;IACxB,0FAA0F;IAC1F,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjE,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CACd,MAAM,CACF,aAAa,GACb,MAAM,GACN,cAAc,GACd,WAAW,GACX,QAAQ,GACR,WAAW,GACX,aAAa,EACf,OAAO,CACR,CACF,CAAC;IACF,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,iBAAkB,SAAQ,gBAAgB;IACzD,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,KAAK,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9D;AAmFD;;;GAGG;AACH,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,iBAAiB,EACtB,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,iBAAiB,CAAC,CAgC5B"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.triageDemoClarifications = triageDemoClarifications;
|
|
4
|
+
const llmCallerRegistry_1 = require("../agent/llmCallerRegistry");
|
|
5
|
+
const modelRouter_1 = require("../agent/modelRouter");
|
|
6
|
+
const demoQuestion_schema_1 = require("../../schemas/demoQuestion.schema");
|
|
7
|
+
const NO_QUESTIONS = { ask: false, questions: [] };
|
|
8
|
+
function buildTriageSystem() {
|
|
9
|
+
return [
|
|
10
|
+
"You are the intake specialist for an autonomous SaaS product-demo VIDEO generator.",
|
|
11
|
+
"Given what we already know about a product, decide whether asking the user a FEW",
|
|
12
|
+
"targeted clarifying questions would MATERIALLY improve the generated video.",
|
|
13
|
+
"",
|
|
14
|
+
"Ask ONLY when it genuinely helps. If the brief is already clear and complete,",
|
|
15
|
+
"return ask=false with no questions — never ask for the sake of asking. Most",
|
|
16
|
+
"well-described products need ZERO questions.",
|
|
17
|
+
"",
|
|
18
|
+
"Good reasons to ask (max 3, ideally 1–2 questions):",
|
|
19
|
+
"- The product's core value or primary user flow is ambiguous from what's given.",
|
|
20
|
+
"- The user attached video/image assets but didn't say how to feature them.",
|
|
21
|
+
"- Which specific features to spotlight is unclear and there are clearly several.",
|
|
22
|
+
"- A concrete creative fork that changes the video and isn't already decided",
|
|
23
|
+
" (e.g. 'lead with the problem or the product?', 'show real UI or stylized?').",
|
|
24
|
+
"",
|
|
25
|
+
"NEVER ask:",
|
|
26
|
+
"- Anything already provided or already pinned by the user (you'll be told which).",
|
|
27
|
+
"- The generic style/pace/length/aspect/color/audio axes — a separate UI covers",
|
|
28
|
+
" those; only ask about them if a pinned value conflicts with the product.",
|
|
29
|
+
"- Anything you can reasonably infer yourself — you are meant to be autonomous.",
|
|
30
|
+
"",
|
|
31
|
+
"Question kinds (use the simplest that fits):",
|
|
32
|
+
'- single: pick one option. Fields: kind,id,prompt,help?,options[{value,label,description?}],allowOther?,required?',
|
|
33
|
+
"- multi: pick several. Same as single plus min?/max?.",
|
|
34
|
+
'- toggle: a yes/no. Fields: kind,id,prompt,help?,default,onLabel?,offLabel?.',
|
|
35
|
+
'- text: a short typed answer. Fields: kind,id,prompt,help?,placeholder?,multiline?,maxLength?.',
|
|
36
|
+
"",
|
|
37
|
+
"Rules: give every question a short stable snake_case id. Keep prompts under ~14",
|
|
38
|
+
"words. Provide 2–5 concrete options for single/multi. Mark a question required",
|
|
39
|
+
"only if the video truly can't proceed without it (rare — prefer optional).",
|
|
40
|
+
"Return valid JSON matching the schema.",
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
function buildTriageUser(ctx) {
|
|
44
|
+
const lines = [];
|
|
45
|
+
lines.push(`Product name: ${ctx.productName}`);
|
|
46
|
+
if (ctx.tagline)
|
|
47
|
+
lines.push(`Tagline: ${ctx.tagline}`);
|
|
48
|
+
if (ctx.description)
|
|
49
|
+
lines.push(`Description: ${ctx.description}`);
|
|
50
|
+
if (ctx.audience)
|
|
51
|
+
lines.push(`Audience: ${ctx.audience}`);
|
|
52
|
+
if (ctx.websiteUrl)
|
|
53
|
+
lines.push(`Website: ${ctx.websiteUrl}`);
|
|
54
|
+
if (ctx.features?.length)
|
|
55
|
+
lines.push(`Features to highlight: ${ctx.features.join(", ")}`);
|
|
56
|
+
const sources = [];
|
|
57
|
+
if (ctx.hasWebsite)
|
|
58
|
+
sources.push("website");
|
|
59
|
+
if (ctx.hasGithub)
|
|
60
|
+
sources.push("github repo");
|
|
61
|
+
if (ctx.hasScreenshots)
|
|
62
|
+
sources.push("screenshots");
|
|
63
|
+
lines.push(`Input sources available: ${sources.length ? sources.join(", ") : "none beyond the text above"}`);
|
|
64
|
+
if (ctx.mediaAssets?.length) {
|
|
65
|
+
const summary = ctx.mediaAssets
|
|
66
|
+
.map((a) => `${a.kind}${a.label ? ` ("${a.label}")` : ""}`)
|
|
67
|
+
.join(", ");
|
|
68
|
+
lines.push(`Featured media the user attached (must appear IN the video): ${summary}.`, "If it's unclear where/how to use these, that is a good thing to ask about.");
|
|
69
|
+
}
|
|
70
|
+
const pinned = Object.entries(ctx.pinned ?? {})
|
|
71
|
+
.filter(([, v]) => v)
|
|
72
|
+
.map(([k]) => k);
|
|
73
|
+
lines.push(`Already decided by the user (do NOT ask about these): ${pinned.length ? pinned.join(", ") : "none"}.`);
|
|
74
|
+
if (ctx.pageSummary) {
|
|
75
|
+
lines.push("", "Extra signal from the site:", ctx.pageSummary.slice(0, 1500));
|
|
76
|
+
}
|
|
77
|
+
lines.push("", "Decide: ask=true with 1–3 high-value questions, or ask=false if the brief is clear.");
|
|
78
|
+
return lines.join("\n");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Run the triage pass. Returns `{ ask, reason, questions }`. Never throws — an
|
|
82
|
+
* unconfigured/erroring LLM degrades to `{ ask:false }` so generation proceeds.
|
|
83
|
+
*/
|
|
84
|
+
async function triageDemoClarifications(ctx, deps = {}) {
|
|
85
|
+
const llm = deps.llm ?? (0, llmCallerRegistry_1.getLlmCaller)();
|
|
86
|
+
const router = deps.router ?? new modelRouter_1.ModelRouter();
|
|
87
|
+
const model = router.pickFor("router");
|
|
88
|
+
const fallbackModel = router.fallbackFor("router");
|
|
89
|
+
try {
|
|
90
|
+
const result = await llm.structured({
|
|
91
|
+
model,
|
|
92
|
+
fallbackModel,
|
|
93
|
+
messages: [
|
|
94
|
+
{ role: "system", content: buildTriageSystem() },
|
|
95
|
+
{ role: "user", content: buildTriageUser(ctx) },
|
|
96
|
+
],
|
|
97
|
+
schema: demoQuestion_schema_1.DemoTriageResultSchema,
|
|
98
|
+
schemaName: "DemoTriage",
|
|
99
|
+
temperature: 0.3,
|
|
100
|
+
maxTokens: 900,
|
|
101
|
+
});
|
|
102
|
+
const parsed = demoQuestion_schema_1.DemoTriageResultSchema.parse(result.data);
|
|
103
|
+
// Enforce our own guardrails regardless of what the model returned.
|
|
104
|
+
const questions = parsed.ask ? dedupeAndCap(parsed.questions) : [];
|
|
105
|
+
return {
|
|
106
|
+
ask: questions.length > 0,
|
|
107
|
+
...(parsed.reason ? { reason: parsed.reason } : {}),
|
|
108
|
+
questions,
|
|
109
|
+
modelUsed: result.usedFallback && fallbackModel ? fallbackModel : model,
|
|
110
|
+
usage: result.usage,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return NO_QUESTIONS;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Drop duplicate ids and cap the set (defense-in-depth over the schema's max). */
|
|
118
|
+
function dedupeAndCap(questions, cap = 3) {
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const q of questions) {
|
|
122
|
+
if (seen.has(q.id))
|
|
123
|
+
continue;
|
|
124
|
+
seen.add(q.id);
|
|
125
|
+
out.push(q);
|
|
126
|
+
if (out.length >= cap)
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth App client — the v2 "connect GitHub for private repos" provider
|
|
3
|
+
* (plan §5, Phase 9). VidSpotAI is the OAuth CLIENT; GitHub is the provider. A
|
|
4
|
+
* classic OAuth App 3-legged flow: authorize → exchange code → non-expiring
|
|
5
|
+
* access token. We request `repo` (private-repo read) + `read:user` (identity).
|
|
6
|
+
*
|
|
7
|
+
* DORMANT until provisioned: everything gates on `GITHUB_OAUTH_CLIENT_ID` +
|
|
8
|
+
* `GITHUB_OAUTH_CLIENT_SECRET`. Until an app is registered and those land in
|
|
9
|
+
* Doppler, `isGithubOAuthConfigured()` is false and the connect endpoint 503s —
|
|
10
|
+
* exactly like the rest of the demo feature ships behind a flag.
|
|
11
|
+
*
|
|
12
|
+
* Env:
|
|
13
|
+
* GITHUB_OAUTH_CLIENT_ID — the OAuth App client id
|
|
14
|
+
* GITHUB_OAUTH_CLIENT_SECRET — the OAuth App client secret
|
|
15
|
+
* GITHUB_OAUTH_REDIRECT_URI — must EXACTLY match the app's Authorization
|
|
16
|
+
* callback URL, e.g.
|
|
17
|
+
* https://<api-host>/v1/demo-video/github/callback
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Scopes we request. `repo` is the classic scope that grants private-repo
|
|
21
|
+
* access (GitHub OAuth Apps have no read-only private-repo scope — that
|
|
22
|
+
* granularity requires a GitHub App installation, a much larger integration);
|
|
23
|
+
* `read:user` is identity only. Documented as broad in the plan §10 risks.
|
|
24
|
+
*/
|
|
25
|
+
export declare const GITHUB_OAUTH_SCOPES: readonly ["read:user", "repo"];
|
|
26
|
+
/** True once both client id + secret are configured. Gate the connect flow on this. */
|
|
27
|
+
export declare function isGithubOAuthConfigured(): boolean;
|
|
28
|
+
/** The registered callback URL. Must byte-match the OAuth App's setting. */
|
|
29
|
+
export declare function githubRedirectUri(): string;
|
|
30
|
+
/** Build the GitHub authorize URL the client opens to start consent. */
|
|
31
|
+
export declare function buildGithubAuthUrl(state: string): string;
|
|
32
|
+
export interface GithubTokenResult {
|
|
33
|
+
accessToken: string;
|
|
34
|
+
/** Scopes GitHub actually granted (may be a subset of requested). */
|
|
35
|
+
scopes: string[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Exchange the authorization `code` for an access token. Throws on any
|
|
39
|
+
* error response (GitHub returns 200 with an `error` field on failure).
|
|
40
|
+
*/
|
|
41
|
+
export declare function exchangeGithubCode(code: string): Promise<GithubTokenResult>;
|
|
42
|
+
export interface GithubUser {
|
|
43
|
+
id: number;
|
|
44
|
+
login: string;
|
|
45
|
+
avatarUrl?: string;
|
|
46
|
+
}
|
|
47
|
+
/** Fetch the authenticated user's identity for display. Throws on failure. */
|
|
48
|
+
export declare function fetchGithubUser(accessToken: string): Promise<GithubUser>;
|
|
49
|
+
export interface GithubRepoSummary {
|
|
50
|
+
/** "owner/name" — what we store as the repo URL basis. */
|
|
51
|
+
fullName: string;
|
|
52
|
+
name: string;
|
|
53
|
+
owner: string;
|
|
54
|
+
private: boolean;
|
|
55
|
+
description?: string;
|
|
56
|
+
/** Default branch (e.g. "main") — handy for the contents API later. */
|
|
57
|
+
defaultBranch: string;
|
|
58
|
+
htmlUrl: string;
|
|
59
|
+
/** ISO timestamp of last push, for "sort by recently active" in the picker. */
|
|
60
|
+
updatedAt?: string;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* List the connected user's repositories for the repo picker (plan §9 UX).
|
|
64
|
+
* Mirrors `fetchGithubUser`'s auth headers against `GET /user/repos`. Pulls up
|
|
65
|
+
* to `maxPages` pages of 100, sorted by most-recently pushed, spanning repos the
|
|
66
|
+
* user owns / collaborates on / can see via org membership. Best-effort per page:
|
|
67
|
+
* a mid-pagination failure returns what we have rather than throwing.
|
|
68
|
+
*/
|
|
69
|
+
export declare function listGithubRepos(accessToken: string, opts?: {
|
|
70
|
+
maxPages?: number;
|
|
71
|
+
}): Promise<GithubRepoSummary[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Best-effort provider-side revoke of a token grant (called on disconnect).
|
|
74
|
+
* Uses the OAuth App's client credentials via Basic auth. Non-fatal — returns
|
|
75
|
+
* false instead of throwing so a disconnect always completes locally.
|
|
76
|
+
*/
|
|
77
|
+
export declare function revokeGithubToken(accessToken: string): Promise<boolean>;
|
|
78
|
+
//# sourceMappingURL=githubOAuth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"githubOAuth.d.ts","sourceRoot":"","sources":["../../../src/services/demo/githubOAuth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,gCAAiC,CAAC;AAIlE,uFAAuF;AACvF,wBAAgB,uBAAuB,IAAI,OAAO,CAKjD;AAcD,4EAA4E;AAC5E,wBAAgB,iBAAiB,IAAI,MAAM,CAI1C;AAED,wEAAwE;AACxE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUxD;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,CAAC,CAoC5B;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,8EAA8E;AAC9E,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAwB9E;AAED,MAAM,WAAW,iBAAiB;IAChC,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,WAAW,EAAE,MAAM,EACnB,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/B,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA8C9B;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAsB7E"}
|