@spark-apps/quickpeek 1.1.2 → 1.2.0

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/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -182,6 +182,10 @@ Edit `demo/plan.json` to customize your demo. Each step uses one of these action
182
182
  | `demo/demo.mp4` | Final video |
183
183
  | `demo/plan.json` | Editable script (re-run to apply changes) |
184
184
 
185
+ > `plan.json` is also accepted by [VidLet](https://vidlet.app)'s `create_project`
186
+ > MCP tool, which turns the steps into a `.vidlet` timeline project for further
187
+ > editing and narration.
188
+
185
189
  ## 🌱 Support & Contributions
186
190
 
187
191
  ⭐ **Star the repo** & I power up like Mario 🍄
package/dist/assets/qp.wm CHANGED
File without changes
File without changes
@@ -0,0 +1,306 @@
1
+ import { U as UserTier, C as Config } from './scraping-BK8pIJhm.mjs';
2
+ export { A as AIError, a as AIResponse, b as AIResult, c as CONFIG_FILE, d as CaptionStyle, e as CaptionWordStyle, D as DEFAULT_CAPTIONS, f as DEFAULT_CONFIG, E as EXCLUDED_LINK_PATTERNS, g as ElementInfo, h as ElementType, F as FEATURES, H as HighlightMode, I as INTERACTIVE_SELECTORS, P as Plan, R as RateLimitInfo, S as SparkStatus, i as SparkSubscription, j as SparkTrial, k as Step, l as SuggestedAction, T as Tier, V as VERSION, m as buildSystemPrompt, n as buildUserPrompt, o as callAI, p as callAIViaRelay, q as crawlPage, r as getErrorMessage, s as getLanguageName, t as getStatus, u as getStatusCached, v as getTierByEmail, w as hasAccess, x as hasTrialRemaining, y as isCanceling, z as isExcludedLink, B as isPaid, G as isVerified, J as normalizeUrl, K as parseAIPlanResponse, L as parsePartial, M as pricingUrl, N as rgbaToHex, O as shouldSkipLink, Q as showPaywall, W as toTitleCase } from './scraping-BK8pIJhm.mjs';
3
+ export { WIDE_VOICES as LANGUAGE_VOICES, hexToAss as hexToASS, windowsDrivePathToWsl as windowsPathToWSL } from '@spark-apps/video-kit';
4
+
5
+ /**
6
+ * Billing utilities — watermark decisions and upgrade URL generation
7
+ */
8
+
9
+ /**
10
+ * Check if watermark should be applied
11
+ * Watermark applies to free tier videos longer than 20 seconds
12
+ */
13
+ declare function shouldWatermark(tier: UserTier, videoDurationSeconds: number): boolean;
14
+ /**
15
+ * Get upgrade URL for pricing page
16
+ */
17
+ declare function getUpgradeUrl(userEmail?: string): string;
18
+
19
+ /**
20
+ * Cursor PNG as base64 data URI for browser injection
21
+ * Source: src/imgs/cursor.png
22
+ */
23
+ declare const CURSOR_PNG = "data:image/png;base64,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";
24
+
25
+ interface StepTransition {
26
+ timestamp: number;
27
+ type: 'fade' | 'blend';
28
+ }
29
+ /** Step sync data for time-stretching video segments to match audio */
30
+ interface StepSync {
31
+ actualStart: number;
32
+ actualEnd: number;
33
+ targetDuration: number;
34
+ }
35
+ interface ComposeOptions {
36
+ videoPath: string;
37
+ assPath: string;
38
+ outputPath: string;
39
+ voiceoverPath?: string | undefined;
40
+ musicPath?: string | undefined;
41
+ musicVolume?: number | undefined;
42
+ musicFadeIn?: number | undefined;
43
+ musicFadeOut?: number | undefined;
44
+ width: number;
45
+ height: number;
46
+ fps: number;
47
+ preset: string;
48
+ quality: number;
49
+ fadeIn: number;
50
+ fadeOut: number;
51
+ videoDuration: number;
52
+ audioBitrate?: string | undefined;
53
+ contrast?: number | undefined;
54
+ trimStart?: number | undefined;
55
+ userTier?: UserTier | undefined;
56
+ mainContentDuration?: number | undefined;
57
+ stepSync?: StepSync[] | undefined;
58
+ stepTransitions?: StepTransition[] | undefined;
59
+ }
60
+ interface ConcatVideoOptions {
61
+ inputPath: string;
62
+ outroPath: string;
63
+ outputPath: string;
64
+ width: number;
65
+ height: number;
66
+ preset: string;
67
+ }
68
+ interface VideoOverlay {
69
+ path: string;
70
+ startTime: number;
71
+ duration: number;
72
+ mode: 'stretch' | 'center';
73
+ trimStart?: number;
74
+ trimEnd?: number;
75
+ }
76
+ interface SpliceVideosOptions {
77
+ inputPath: string;
78
+ outputPath: string;
79
+ overlays: VideoOverlay[];
80
+ width: number;
81
+ height: number;
82
+ preset: string;
83
+ }
84
+ interface SpeedUpOptions {
85
+ inputPath: string;
86
+ outputPath: string;
87
+ factor: number;
88
+ preset: string;
89
+ }
90
+ interface BlendTransition {
91
+ timestamp: number;
92
+ type: 'fade' | 'blend';
93
+ duration?: number;
94
+ }
95
+ interface ApplyTransitionsOptions {
96
+ inputVideo: string;
97
+ outputVideo: string;
98
+ transitions: BlendTransition[];
99
+ videoDuration: number;
100
+ preset?: string;
101
+ }
102
+ /** A time segment with start and end positions in seconds */
103
+ interface TimeSegment {
104
+ start: number;
105
+ end: number;
106
+ }
107
+
108
+ /**
109
+ * Compose final video with subtitles, voiceover, and music
110
+ */
111
+ declare function composeVideo(opts: ComposeOptions): Promise<void>;
112
+
113
+ /**
114
+ * Build segments to keep around action timestamps
115
+ * Merges overlapping windows automatically
116
+ */
117
+ declare function buildKeepSegments(eventTimestampsSeconds: number[], totalDurationSeconds: number, preWindow?: number, postWindow?: number): TimeSegment[];
118
+ /**
119
+ * Map original event timestamps to their positions in the compressed video
120
+ */
121
+ declare function mapTimestampsAfterCompression(originalTimestampsSeconds: number[], segments: TimeSegment[]): number[];
122
+
123
+ /**
124
+ * Run ffprobe to get media duration in milliseconds
125
+ */
126
+ declare function getMediaDuration(filePath: string): Promise<number>;
127
+ /**
128
+ * Concatenate audio files using ffmpeg concat demuxer
129
+ */
130
+ declare function concatMedia(listPath: string, output: string): Promise<void>;
131
+ /**
132
+ * Generate silence audio file of specified duration
133
+ */
134
+ declare function generateSilence(outputPath: string, durationMs: number): Promise<void>;
135
+ /**
136
+ * Speed up video by a factor
137
+ */
138
+ declare function speedUpVideo(opts: SpeedUpOptions): Promise<void>;
139
+ /**
140
+ * Concatenate main video with outro (video streams only, no audio processing)
141
+ * The outro is scaled to fit the main video dimensions and centered
142
+ * Returns the combined video for further processing (composition with audio)
143
+ */
144
+ declare function concatVideoWithOutro(opts: ConcatVideoOptions): Promise<void>;
145
+
146
+ /**
147
+ * Splice video files into the recording at specific timestamps
148
+ * Replaces dark placeholder sections with actual video content
149
+ */
150
+ declare function spliceVideos(opts: SpliceVideosOptions): Promise<void>;
151
+
152
+ /**
153
+ * Build step transitions array from steps and durations
154
+ * @param steps - Array of steps with optional transition field
155
+ * @param stepDurations - Duration of each step in milliseconds
156
+ * @param defaultTransition - Config-level default (applied to all steps except first)
157
+ */
158
+ declare function buildStepTransitions(steps: {
159
+ transition?: 'fade' | 'blend';
160
+ }[], stepDurations: number[], defaultTransition?: 'none' | 'fade' | 'blend'): StepTransition[];
161
+ /**
162
+ * Apply fast step transitions using FFmpeg xfade filter
163
+ *
164
+ * Uses single-pass filter graph:
165
+ * 1. Split input into segments at transition points using trim
166
+ * 2. Chain segments with xfade for smooth transitions
167
+ * 3. Single encode pass - fast and efficient
168
+ *
169
+ * Transition types:
170
+ * - 'blend': Cross-dissolve between frames (xfade=fade)
171
+ * - 'fade': Dip to black and back (xfade=fadeblack)
172
+ */
173
+ declare function applyTransitions(opts: ApplyTransitionsOptions): Promise<void>;
174
+
175
+ declare const CURSOR_CSS = "\n #qp-cursor {\n position: fixed;\n width: 32px;\n height: 38px;\n background: url(\"data:image/png;base64,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\") no-repeat;\n background-size: contain;\n pointer-events: none;\n z-index: 2147483647;\n transform: translate(-3px, -3px);\n transition: left 800ms ease-out, top 800ms ease-out, transform 0.08s ease-out;\n filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.3));\n }\n #qp-cursor.clicking {\n transform: translate(-3px, -3px) scale(0.9);\n }\n";
176
+ /**
177
+ * JavaScript to inject for cursor tracking
178
+ */
179
+ declare const CURSOR_SCRIPT = "\n (function() {\n if (document.getElementById('qp-cursor')) return;\n const cursor = document.createElement('div');\n cursor.id = 'qp-cursor';\n document.body.appendChild(cursor);\n\n document.addEventListener('mousemove', (e) => {\n cursor.style.left = e.clientX + 'px';\n cursor.style.top = e.clientY + 'px';\n });\n\n document.addEventListener('mousedown', () => cursor.classList.add('clicking'));\n document.addEventListener('mouseup', () => cursor.classList.remove('clicking'));\n\n // Start at center\n cursor.style.left = window.innerWidth / 2 + 'px';\n cursor.style.top = window.innerHeight / 2 + 'px';\n })();\n";
180
+ /**
181
+ * Generate ASS subtitle file header
182
+ */
183
+ declare function generateASSHeader(width: number, height: number, font: string, size: number, primaryColor: string, secondaryColor: string, outlineColor: string, backColor: string, bold: boolean, outlineWidth: number, shadow: number, marginBottom: number): string;
184
+
185
+ /**
186
+ * Subscription and user registration client
187
+ * CLI-only headless registration fallback. Tier/verification checks
188
+ * are now in the SparkStripe gating module (getTierByEmail, isVerified).
189
+ */
190
+ interface RegistrationResult {
191
+ success: boolean;
192
+ pendingVerification?: boolean;
193
+ alreadyVerified?: boolean;
194
+ error?: string;
195
+ }
196
+ /**
197
+ * Register user for free plan via SparkStripe
198
+ */
199
+ declare function registerFreeUser(email: string): Promise<RegistrationResult>;
200
+
201
+ /** Minimal step shape needed by TTS modules — compatible with full Step from plan.ts */
202
+ interface TTSStep {
203
+ caption: string;
204
+ action?: string;
205
+ target?: string;
206
+ value?: string;
207
+ partial?: string;
208
+ duration?: number;
209
+ }
210
+ interface WordTiming {
211
+ word: string;
212
+ start: number;
213
+ end: number;
214
+ }
215
+ interface StepAudio {
216
+ duration: number;
217
+ words: WordTiming[];
218
+ }
219
+ interface AudioResult {
220
+ voiceoverPath: string;
221
+ stepAudio: Map<number, StepAudio>;
222
+ }
223
+ /**
224
+ * Metadata stored alongside voiceover for reuse detection
225
+ */
226
+ interface VoiceoverMeta {
227
+ captionsHash: string;
228
+ totalCaptionLength: number;
229
+ stepCount: number;
230
+ voice: string;
231
+ stepDurations: Record<number, number>;
232
+ }
233
+
234
+ /**
235
+ * Load existing voiceover metadata if available
236
+ */
237
+ declare function loadVoiceoverMeta(outputDir: string): Promise<VoiceoverMeta | null>;
238
+
239
+ interface Step {
240
+ caption: string;
241
+ action?: string;
242
+ target?: string;
243
+ value?: string;
244
+ partial?: string;
245
+ duration?: number;
246
+ }
247
+ /**
248
+ * Check if existing voiceover can be reused
249
+ */
250
+ declare function canReuseVoiceover(steps: Step[], voice: string, existingMeta: VoiceoverMeta | null): {
251
+ canReuse: boolean;
252
+ reason?: string;
253
+ };
254
+ /**
255
+ * Load existing voiceover and reconstruct stepAudio from metadata
256
+ */
257
+ declare function loadExistingVoiceover(steps: Step[], outputDir: string, meta: VoiceoverMeta, config: {
258
+ timing: {
259
+ minStepDuration: number;
260
+ };
261
+ }): Promise<{
262
+ voiceoverPath: string;
263
+ stepAudio: Map<number, {
264
+ duration: number;
265
+ words: {
266
+ word: string;
267
+ start: number;
268
+ end: number;
269
+ }[];
270
+ }>;
271
+ }>;
272
+
273
+ declare function resolveVoice(config: Config): string | null;
274
+ declare function generateVoiceover(steps: TTSStep[], config: Config, workDir: string, outputDir: string, logError: (context: string, error: unknown) => Promise<void>, task: (text: string) => {
275
+ succeed: (text?: string) => void;
276
+ }, info: (text: string) => void): Promise<AudioResult>;
277
+
278
+ /**
279
+ * Synthesize speech using Python edge-tts (more reliable than Node.js msedge-tts)
280
+ * Falls back to Node.js msedge-tts if Python is unavailable
281
+ */
282
+ declare function synthesizeSpeech(text: string, outputPath: string, voice: string): Promise<void>;
283
+
284
+ declare function estimateWordTimings(caption: string, duration: number): WordTiming[];
285
+ /**
286
+ * Align voiceover to actual recording durations
287
+ * If any step's recording took longer than its voiceover, pad with silence
288
+ * Returns updated stepAudio with aligned durations
289
+ */
290
+ declare function alignVoiceoverToRecording(stepAudio: Map<number, StepAudio>, actualRecordingDurations: number[], workDir: string, outputDir: string): Promise<Map<number, StepAudio>>;
291
+
292
+ /**
293
+ * FFmpeg watermark filter generation for QuickPeek
294
+ * Applies watermarks to free tier videos longer than 20 seconds
295
+ */
296
+
297
+ /**
298
+ * Build FFmpeg drawtext filters for watermarks
299
+ * Returns empty string if no watermark needed (pro tier or short video)
300
+ */
301
+ declare function buildWatermarkFilters(opts: {
302
+ tier: UserTier;
303
+ videoDuration: number;
304
+ }): string;
305
+
306
+ export { type ApplyTransitionsOptions, type AudioResult, type BlendTransition, CURSOR_CSS, CURSOR_PNG, CURSOR_SCRIPT, type ComposeOptions, type ConcatVideoOptions, Config, type SpeedUpOptions, type SpliceVideosOptions, type StepAudio, type StepSync, type StepTransition, type TTSStep, type TimeSegment, UserTier, type VideoOverlay, type VoiceoverMeta, type WordTiming, alignVoiceoverToRecording, applyTransitions, buildKeepSegments, buildStepTransitions, buildWatermarkFilters, canReuseVoiceover, composeVideo, concatMedia, concatVideoWithOutro, estimateWordTimings, generateASSHeader, generateSilence, generateVoiceover, getMediaDuration, getUpgradeUrl, loadExistingVoiceover, loadVoiceoverMeta, mapTimestampsAfterCompression, registerFreeUser, resolveVoice, shouldWatermark, speedUpVideo, spliceVideos, synthesizeSpeech };