@spark-apps/quickpeek 1.2.3 → 1.2.4

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.
@@ -0,0 +1,613 @@
1
+ import { Page } from 'playwright';
2
+
3
+ /**
4
+ * Configuration types and defaults for QuickPeek
5
+ */
6
+ declare const VERSION = "1.2.3";
7
+ declare const CONFIG_FILE = "quickpeek.config.json";
8
+ type UserTier = 'free' | 'pro' | 'elite';
9
+ /** The one dimension knob users see: wide (16:9) or short (9:16). */
10
+ type VideoSize = 'wide' | 'short';
11
+ /** Narration pace, mapped to an Edge TTS rate. */
12
+ type VoiceRate = 'slow' | 'normal' | 'fast';
13
+ type VoiceGender = 'female' | 'male';
14
+ /**
15
+ * What each size means in pixels. `zoom` shrinks the CSS viewport so a
16
+ * portrait page hits its mobile breakpoints and text stays legible; wide
17
+ * records at desktop layout untouched.
18
+ */
19
+ declare const SIZE_PRESETS: Record<VideoSize, {
20
+ width: number;
21
+ height: number;
22
+ zoom: number;
23
+ }>;
24
+ /**
25
+ * The three named paces as multipliers of the voice's natural rate.
26
+ *
27
+ * A multiplier rather than the edge-tts "-20%" string it used to be, so the
28
+ * named pace can COMPOSE with the profile's numeric `audio.speed` instead of
29
+ * excluding it - see the note in tts/generate.ts, where excluding it made
30
+ * `audio.speed` dead code and took the profiles' narration pace with it.
31
+ */
32
+ declare const RATE_FACTOR: Record<VoiceRate, number>;
33
+ /**
34
+ * Set as video.outro when the closing card is already a plan step.
35
+ *
36
+ * Lives here rather than with the phase that sets it because compose reads it
37
+ * too: a run that ends on a credits card must not also stamp the free-tier
38
+ * end text over that card.
39
+ */
40
+ declare const NO_CLIP_OUTRO = "@card-step";
41
+ interface Config {
42
+ lang?: string;
43
+ plan?: {
44
+ maxSteps: number;
45
+ };
46
+ browser?: {
47
+ extension?: string;
48
+ };
49
+ profile?: 'wide' | 'short';
50
+ video: {
51
+ size: VideoSize;
52
+ width: number;
53
+ height: number;
54
+ format: string;
55
+ preset: 'ultrafast' | 'superfast' | 'veryfast' | 'faster' | 'fast' | 'medium' | 'slow' | 'slower' | 'veryslow';
56
+ quality: number;
57
+ fps: number;
58
+ cursor: boolean;
59
+ highlight: false | 'outline' | 'full';
60
+ fadeIn: number;
61
+ fadeOut: number;
62
+ transitions: 'none' | 'fade' | 'blend';
63
+ contrast: number;
64
+ tempo: number;
65
+ stepDelay: number;
66
+ outro?: string;
67
+ outroCard?: {
68
+ /** The app the video is about - the card's headline. */
69
+ subject: {
70
+ name: string;
71
+ tagline?: string;
72
+ domain?: string;
73
+ iconPath?: string;
74
+ };
75
+ /** Tools that made the video, shown small in the corner. */
76
+ credits?: Array<{
77
+ name: string;
78
+ role: string;
79
+ iconPath?: string;
80
+ }>;
81
+ promo?: {
82
+ code: string;
83
+ percent: number;
84
+ site?: string;
85
+ };
86
+ /**
87
+ * Say "free to start" on the closing card. Default true.
88
+ *
89
+ * Almost every product this records is freemium, and the free tier is
90
+ * the one closing line that never expires the way a discount code does,
91
+ * so it is opt-out rather than opt-in. Set false for a paid-only product.
92
+ */
93
+ freeTier?: boolean;
94
+ seconds?: number;
95
+ };
96
+ zoom?: number;
97
+ maxSpeedup?: number;
98
+ autoPan?: boolean;
99
+ introCard?: boolean;
100
+ outroCardEnabled?: boolean;
101
+ introCardSeconds?: number;
102
+ maxSilenceSecs?: number;
103
+ fitLimitSecs?: number;
104
+ captions?: Partial<CaptionStyle>;
105
+ hwaccel?: 'auto' | 'off';
106
+ captionsBurn?: boolean;
107
+ };
108
+ audio?: {
109
+ voice?: string;
110
+ gender?: VoiceGender;
111
+ rate?: VoiceRate;
112
+ format: string;
113
+ bitrate: string;
114
+ speed?: number;
115
+ voiceover?: boolean;
116
+ voiceClipsDir?: string;
117
+ ambience?: boolean;
118
+ transcribe?: boolean;
119
+ };
120
+ music?: {
121
+ path: string;
122
+ volume: number;
123
+ /**
124
+ * Which bundled CC0 bed to score with when no `path` is given.
125
+ *
126
+ * 'calm' | 'gentle' | 'lofi' - see assets/music/manifest.json. An unknown
127
+ * mood falls back to the default rather than dropping the music.
128
+ */
129
+ mood?: string;
130
+ };
131
+ timing: {
132
+ minStepDuration: number;
133
+ /**
134
+ * Ceiling on a step's hold, in ms. Undefined means none.
135
+ *
136
+ * A demo is filmed in real time, so its holds ARE the recording time: a
137
+ * 75s video costs 75s to shoot no matter what the encoder does. Capping
138
+ * them is the only thing that makes a draft quick, and it is the one
139
+ * trade a draft can afford, since the question a draft answers is whether
140
+ * the demo clicks the right things in the right order.
141
+ */
142
+ maxStepDuration?: number;
143
+ /**
144
+ * How long an action waits for the page to paint, in ms.
145
+ *
146
+ * The recording floor is not the holds, it is the page loads: eleven
147
+ * navigations each waiting on content is most of a draft's runtime. A
148
+ * draft is allowed to film a half-drawn page; a deliverable is not.
149
+ */
150
+ paintBudget?: number;
151
+ };
152
+ output: {
153
+ dir: string;
154
+ };
155
+ }
156
+ interface CaptionWordStyle {
157
+ color: string;
158
+ size: number;
159
+ bold: boolean;
160
+ }
161
+ type CaptionPreset = 'karaoke' | 'hormozi' | 'shorts' | 'minimal';
162
+ type CaptionPosition = 'bottom' | 'center' | 'top';
163
+ interface CaptionStyle {
164
+ font: string;
165
+ outline: string;
166
+ outlineWidth: number;
167
+ shadow: number;
168
+ marginBottom: number;
169
+ preset: CaptionPreset;
170
+ position: CaptionPosition;
171
+ regular: CaptionWordStyle;
172
+ highlight: CaptionWordStyle;
173
+ /**
174
+ * Characters a caption line may reach before it wraps.
175
+ *
176
+ * A ceiling, not a target: the renderer narrows it further to whatever the
177
+ * frame actually fits at this font size. It exists because "fits the frame"
178
+ * and "reads at a glance on a phone" are different questions - a 9:16 short
179
+ * wants two short lines where a 16:9 tutorial is happy with one long one.
180
+ */
181
+ maxChars: number;
182
+ }
183
+ declare const DEFAULT_CONFIG: Config;
184
+ /** The two standard deliverables. Every generated demo is one or the other. */
185
+ type VideoProfileName = 'wide' | 'short';
186
+ /**
187
+ * Standard settings per deliverable, applied over the loaded config.
188
+ *
189
+ * wide: a 16:9 educational tutorial. Comprehensive, natural pace - no
190
+ * over-limit squeeze (fitLimitSecs 0), desktop layout (zoom 1.5), narration
191
+ * slightly brisker than the voice's default.
192
+ *
193
+ * short: a 9:16 quick-paced short. Covers every interesting detail even past
194
+ * a minute, then is squeezed toward 60s - but never beyond 1.7x, because past
195
+ * that the fix is a tighter plan, not a faster player.
196
+ */
197
+ declare const VIDEO_PROFILES: Record<VideoProfileName, {
198
+ video: Partial<Config['video']>;
199
+ audioSpeed: number;
200
+ voice?: string;
201
+ }>;
202
+ /** Overlay a standard profile on a loaded config. Explicit knobs win where set. */
203
+ declare function applyProfile(config: Config, profile: VideoProfileName): Config;
204
+ declare const DEFAULT_CAPTIONS: CaptionStyle;
205
+ /**
206
+ * Caption geometry that suits the canvas, expressed as fractions of it.
207
+ *
208
+ * The fixed defaults above were authored once and applied to every shape, and
209
+ * on the 1080x1920 canvas QuickPeek defaults to they are wrong in three ways
210
+ * at once: 56px is 5% of the frame width, which is a desktop subtitle rather
211
+ * than a social caption; 80px of bottom margin puts the words 4% up the frame,
212
+ * underneath the title, channel row and action rail every phone player draws
213
+ * over the bottom of a vertical video; and 28 characters at that size is a
214
+ * single long line the eye has to track rather than a short stack it can take
215
+ * in at a glance.
216
+ *
217
+ * Fractions rather than pixels so the same reasoning holds at 720x1280 or
218
+ * 1440x2560. The portrait numbers: text at 21% of frame height clears the
219
+ * player chrome on YouTube Shorts, TikTok and Reels while staying in the lower
220
+ * third, and ~9% of frame width is the size those platforms' own captions use.
221
+ */
222
+ declare function defaultCaptionsFor(width: number, height: number): CaptionStyle;
223
+
224
+ /**
225
+ * AI API client — Spark AI communication
226
+ * Handles token management, rate limiting, and retries
227
+ *
228
+ * Two transports:
229
+ * - Relay (default for the published CLI): quickpeek-web mints a short-lived
230
+ * per-user token from a registered email and proxies the chat server-side.
231
+ * No credential ships in the npm bundle.
232
+ * - Direct (dev/server only): when SPARK_AI_KEY is present in the runtime
233
+ * environment (never baked at build time), call Spark AI directly. This is
234
+ * the path quickpeek-web itself uses server-side.
235
+ */
236
+
237
+ interface RateLimitInfo {
238
+ used: number;
239
+ limit: number;
240
+ tier: UserTier;
241
+ resetAt?: string;
242
+ }
243
+ interface AIResponse {
244
+ success: true;
245
+ content: string;
246
+ limits: RateLimitInfo;
247
+ }
248
+ interface AIError {
249
+ success: false;
250
+ error: string;
251
+ isRateLimit?: boolean;
252
+ isAuthError?: boolean;
253
+ retryAfterSeconds?: number;
254
+ /** Upstream HTTP status, when the failure was an HTTP response. */
255
+ status?: number;
256
+ /** Host that produced the failure, so relay vs backend is distinguishable. */
257
+ host?: string;
258
+ /** Raw response body (truncated), for the error log. */
259
+ detail?: string;
260
+ }
261
+ type AIResult = AIResponse | AIError;
262
+ /** Called before each backoff so the CLI can explain a long pause. */
263
+ type RetryNotice = (info: {
264
+ attempt: number;
265
+ status: number;
266
+ delayMs: number;
267
+ host: string;
268
+ }) => void;
269
+ interface ChatOptions {
270
+ onRetry?: RetryNotice;
271
+ /**
272
+ * Backoff before each 5xx retry. Defaults to SERVER_BACKOFF_MS, which is
273
+ * sized to fit inside a serverless function budget — callAI runs inside the
274
+ * quickpeek-web routes as well as the CLI. Interactive callers should pass
275
+ * CLI_BACKOFF_MS, which can afford to outwait the backend's closed window.
276
+ */
277
+ backoffMs?: number[];
278
+ /**
279
+ * Completion-token budget to ask the backend for. Omitted, the backend falls
280
+ * back to the app's own default, which is 300 — far too small for the JSON
281
+ * this CLI asks for. See PLAN_MAX_TOKENS.
282
+ */
283
+ maxTokens?: number;
284
+ }
285
+ /**
286
+ * Every AI call this CLI makes is structured generation: a JSON plan with one
287
+ * entry per script line, or one caption per step. The backend defaults to a
288
+ * 300-token completion budget sized for a two-sentence chat reply, and a
289
+ * 14-line plan measured ~300 tokens of JSON on its own, so the default
290
+ * truncated every real run mid-array (salvaged silently by the JSON repair
291
+ * path) and returned nothing at all once the model's own reasoning ate the
292
+ * budget first. Ask for a budget the output actually fits in.
293
+ */
294
+ declare const PLAN_MAX_TOKENS = 1500;
295
+ /** Short enough to stay well inside a serverless maxDuration. */
296
+ declare const SERVER_BACKOFF_MS: number[];
297
+ /**
298
+ * Long enough to outlast the AI backend's ~45-60s closed window (measured
299
+ * 16 Aug 2026). Only safe where a human is waiting on a spinner.
300
+ */
301
+ declare const CLI_BACKOFF_MS: number[];
302
+ /**
303
+ * Direct Spark AI call using the runtime SPARK_AI_KEY (dev + server-side use).
304
+ */
305
+ declare function callAI(systemPrompt: string, userPrompt: string, tier?: UserTier, opts?: ChatOptions): Promise<AIResult>;
306
+ /**
307
+ * Relayed Spark AI call for the published CLI: authenticates with the user's
308
+ * registered email via quickpeek-web, which holds the real key server-side
309
+ * and enforces per-user daily limits.
310
+ */
311
+ declare function callAIViaRelay(systemPrompt: string, userPrompt: string, email: string, tier?: UserTier, opts?: ChatOptions): Promise<AIResult>;
312
+ /**
313
+ * Parse AI-generated plan JSON from response text
314
+ */
315
+ declare function parseAIPlanResponse(content: string): {
316
+ success: true;
317
+ plan: {
318
+ title?: string;
319
+ description?: string;
320
+ steps: Array<{
321
+ id?: number;
322
+ action?: string;
323
+ caption?: string;
324
+ target?: string;
325
+ value?: string;
326
+ }>;
327
+ };
328
+ } | {
329
+ success: false;
330
+ error: string;
331
+ };
332
+ /**
333
+ * Normalize a URL by adding https:// and stripping www prefix
334
+ */
335
+ declare function normalizeUrl(input: string): string;
336
+
337
+ /**
338
+ * Shared types for QuickPeek core
339
+ */
340
+ /** Action type suggested for an element based on its type */
341
+ type SuggestedAction = 'click' | 'type' | 'upload' | 'drag' | 'select' | 'check';
342
+ /** Element type discovered on a page */
343
+ type ElementType = 'button' | 'input' | 'link' | 'text' | 'file' | 'select' | 'range' | 'checkbox';
344
+ /** Discovered element from page crawl */
345
+ interface ElementInfo {
346
+ /** Element type */
347
+ type: ElementType | string;
348
+ /** Display text/label for the element */
349
+ text: string;
350
+ /** CSS selector to target the element */
351
+ selector: string;
352
+ /** Suggested action for this element */
353
+ action?: SuggestedAction;
354
+ /** Simplified target name (just the text, for AI use) */
355
+ target?: string;
356
+ /** Same-origin destination, for links the planner may navigate to. */
357
+ href?: string | undefined;
358
+ }
359
+ /** Highlight mode for UI elements */
360
+ type HighlightMode = false | 'outline' | 'full';
361
+ /** Step in a demo plan */
362
+ interface Step {
363
+ id: number | string;
364
+ action: string;
365
+ target?: string;
366
+ value?: string;
367
+ caption?: string;
368
+ description?: string;
369
+ duration?: number;
370
+ zoom?: number | string;
371
+ partial?: string;
372
+ transition?: 'fade' | 'blend';
373
+ highlight?: HighlightMode;
374
+ mediaFile?: string;
375
+ mediaType?: 'image' | 'video';
376
+ /**
377
+ * Speak this step's caption but do not burn it.
378
+ *
379
+ * For a step whose frame is already type - a title or closing card - where
380
+ * a burned caption lands on top of the words it is repeating.
381
+ */
382
+ spokenOnly?: boolean;
383
+ }
384
+ /** Demo plan structure */
385
+ interface Plan {
386
+ url: string;
387
+ title: string;
388
+ description: string;
389
+ lang?: string;
390
+ steps: Step[];
391
+ }
392
+
393
+ /**
394
+ * Page crawling and interactive element extraction
395
+ */
396
+
397
+ /**
398
+ * Wait until the loading placeholders are gone, or give up and film anyway.
399
+ *
400
+ * waitForDomStable counts elements, and a skeleton shell is exactly the state
401
+ * where that count has already stopped moving: the panels are on screen, they
402
+ * are just grey. So the recorder started step 1 over spinners and the opening
403
+ * seconds of the demo are the app pretending to be itself.
404
+ *
405
+ * Three ways out, and none of them can hang a run: no placeholders (the
406
+ * common case, one evaluate and no wait at all), a count that never budges -
407
+ * which is furniture, a permanent element whose class happens to say spinner,
408
+ * not loading state - or the cap.
409
+ */
410
+ declare function waitForPlaceholdersGone(page: Page, cap?: number): Promise<void>;
411
+ /**
412
+ * Navigate and give the page a bounded chance to finish rendering.
413
+ *
414
+ * `networkidle` as the goto condition strands on pages that poll or stream —
415
+ * a signed-in dashboard rarely goes idle, so goto times out with the page
416
+ * already painted. Land on domcontentloaded, which such pages do reach, then
417
+ * wait for idle only as a best effort, then for the DOM to hold still, and
418
+ * finally for the skeletons to clear - a shell whose element count has
419
+ * settled is still not a page anyone wants filmed.
420
+ */
421
+ declare function gotoSettled(page: Page, url: string, timeout?: number): Promise<void>;
422
+
423
+ /**
424
+ * A selector matching one id.
425
+ *
426
+ * HTML ids are far more permissive than CSS identifiers: `description-(optional)`
427
+ * is a perfectly legal id, but `#description-(optional)` is invalid CSS and
428
+ * Playwright rejects it, so the step silently finds nothing. Anything that is
429
+ * not identifier-safe gets the attribute form instead.
430
+ */
431
+ declare function idSelector(id: string): string;
432
+ /**
433
+ * A selector matching one aria-label.
434
+ *
435
+ * An icon-only button carries its label in `aria-label`, and Playwright's
436
+ * `text=` engine and `:has-text()` both match rendered text — never attributes.
437
+ * A label borrowed from the attribute therefore has to be emitted as one too,
438
+ * or the selector matches nothing at all.
439
+ */
440
+ declare function ariaLabelSelector(label: string): string;
441
+ /**
442
+ * Crawl a page and extract interactive elements with smart selectors
443
+ * Results are cached in-memory for a few minutes to avoid duplicate browser launches
444
+ * @param url - URL to crawl
445
+ * @param task - Optional progress reporter (CLI uses ora spinners, web can omit)
446
+ */
447
+ declare function crawlPage(url: string, task?: (text: string) => {
448
+ succeed: (text?: string) => void;
449
+ }, storageStatePath?: string, viewport?: {
450
+ width: number;
451
+ height: number;
452
+ }): Promise<{
453
+ title: string;
454
+ elements: ElementInfo[];
455
+ scrollable: number;
456
+ }>;
457
+
458
+ /**
459
+ * Utility functions for QuickPeek
460
+ */
461
+ /**
462
+ * Get full language name from ISO code
463
+ */
464
+ declare function getLanguageName(lang?: string): string | null;
465
+
466
+ /**
467
+ * Convert rgba() or rgb() color string to hex
468
+ */
469
+ declare function rgbaToHex(color: string): string;
470
+
471
+ /**
472
+ * Parse partial field "start-end" into trim values
473
+ * e.g., "0-3.5" -> { trimStart: 0, trimEnd: 3.5 }
474
+ * e.g., "2-" -> { trimStart: 2, trimEnd: undefined }
475
+ */
476
+ declare function parsePartial(partial: string | undefined): {
477
+ trimStart?: number;
478
+ trimEnd?: number;
479
+ };
480
+ /**
481
+ * Extract a message string from an unknown error value
482
+ */
483
+ declare function getErrorMessage(err: unknown): string;
484
+ declare function deStock(text: string): string;
485
+ /**
486
+ * Strip dashes a caption should never carry.
487
+ *
488
+ * House rule: no em or en dashes in anything QuickPeek burns or speaks. On
489
+ * screen a long dash at portrait sizes is hard to tell from a hyphen and
490
+ * eats width a short has none of; spoken, edge-tts reads it as a hard stop
491
+ * mid-sentence. A comma does the same job in both places.
492
+ *
493
+ * A dash BETWEEN digits is a range ("2-3x", "10-20%"), so it becomes a
494
+ * hyphen rather than a comma.
495
+ */
496
+ declare function stripLongDashes(text: string): string;
497
+ /**
498
+ * Convert text to Title Case, keeping prepositions/articles lowercase
499
+ * First word is always capitalized
500
+ */
501
+ declare function toTitleCase(text: string): string;
502
+ /**
503
+ * Run `task` for every index in [0, count) with at most `size` in flight.
504
+ *
505
+ * Workers pull from a shared cursor rather than being handed a fixed slice, so
506
+ * one slow item does not leave its worker's remaining share unstarted while
507
+ * others sit idle. Tasks are expected to record their own results by index;
508
+ * nothing is collected here, because the callers that need ordering want to
509
+ * assemble it themselves.
510
+ */
511
+ declare function runPooled(count: number, size: number, task: (index: number) => Promise<void>): Promise<void>;
512
+
513
+ /**
514
+ * AI prompts for demo plan generation
515
+ */
516
+
517
+ declare function buildSystemPrompt(maxSteps: number, profile?: 'wide' | 'short'): string;
518
+ declare function buildUserPrompt(url: string, title: string, elements: ElementInfo[], langName: string | null, description: string | undefined, maxSteps: number, scrollable?: number): string;
519
+
520
+ type Tier = 'free' | 'pro' | 'elite';
521
+ interface SparkSubscription {
522
+ payment_type: 'subscription' | 'one_time';
523
+ status: 'active' | 'trialing' | 'past_due' | 'canceled' | 'lifetime';
524
+ is_lifetime: boolean;
525
+ current_period_end: string;
526
+ cancel_at_period_end: boolean;
527
+ billing_interval: 'monthly' | 'yearly' | 'one_time';
528
+ price_id: string;
529
+ created_at: string;
530
+ }
531
+ interface SparkTrial {
532
+ is_expired: boolean;
533
+ days_remaining: number;
534
+ uses_remaining: number;
535
+ max_days: number;
536
+ max_uses: number;
537
+ }
538
+ interface SparkStatus {
539
+ subscription: SparkSubscription | null;
540
+ verified: boolean;
541
+ registered: boolean;
542
+ usage_count: number;
543
+ trial: SparkTrial | null;
544
+ message?: string;
545
+ }
546
+ declare const getStatus: (email: string) => Promise<SparkStatus>;
547
+ declare function getStatusCached(email: string): Promise<SparkStatus>;
548
+ declare function getTier(d: SparkStatus): Tier;
549
+ declare const isPaid: (d: SparkStatus) => boolean;
550
+ declare const hasTrialRemaining: (d: SparkStatus) => boolean;
551
+ declare const showPaywall: (d: SparkStatus) => boolean;
552
+ declare const isCanceling: (d: SparkStatus) => boolean;
553
+ /** Resolve tier directly from an email — the most common consumer call. Falls back to 'free' on error. */
554
+ declare function getTierByEmail(email: string): Promise<Tier>;
555
+ /** Check whether an email is verified. Falls back to false on error. */
556
+ declare function isVerified(email: string): Promise<boolean>;
557
+ declare function pricingUrl(opts?: {
558
+ email?: string;
559
+ returnUrl?: string;
560
+ }): string;
561
+
562
+ /**
563
+ * Shared constants for page scraping and element filtering
564
+ * Used by both CLI and web versions
565
+ */
566
+ declare const EXCLUDED_LINK_PATTERNS: string[];
567
+ /** CSS selectors for interactive page elements */
568
+ declare const INTERACTIVE_SELECTORS: string[];
569
+ /** Check if link text or href matches an excluded pattern */
570
+ declare function isExcludedLink(text: string, href?: string): boolean;
571
+ /** Check if a link should be skipped during crawling */
572
+ declare function shouldSkipLink(text: string, href: string, currentPath: string): boolean;
573
+
574
+ /**
575
+ * Edge TTS voice selection.
576
+ *
577
+ * QuickPeek uses the kit's WIDE table: 68 languages with standard neural
578
+ * voices and en-GB for English. Breadth matters more than polish here,
579
+ * because a demo gets captioned for whatever audience asked for it. Male
580
+ * voices exist for the 14 major languages of the kit's MALE table; the rest
581
+ * fall back to the standard voice for that language rather than switching
582
+ * language just to keep the gender.
583
+ */
584
+
585
+ /**
586
+ * Asking for the multilingual voices by name rather than by Edge id.
587
+ *
588
+ * `audio.voice` takes an exact voice, which is precise and useless for
589
+ * choosing a SET: the multilingual voice differs per language, so an exact id
590
+ * pins the config to one language. This word resolves per language instead,
591
+ * so the same config narrates a demo in any of them.
592
+ */
593
+ declare const INTERNATIONAL_VOICE = "international";
594
+ /**
595
+ * The multilingual neural voice for a language, e.g. en-US-AvaMultilingualNeural.
596
+ *
597
+ * These read noticeably better than the standard neural voices and carry an
598
+ * accent across languages instead of switching person. The WIDE table is
599
+ * still the fallback: multilingual voices do not exist for every language,
600
+ * and dropping to the standard voice for one is better than no narration.
601
+ */
602
+ declare function internationalVoiceFor(lang: string, gender?: VoiceGender): string | null;
603
+ /** Whether narration exists for this language at all. */
604
+ declare function hasVoice(lang: string): boolean;
605
+ /**
606
+ * The Edge voice for a language + gender, falling back through WIDE_VOICES —
607
+ * or null for a language with no voice at all. Null keeps the deliberate
608
+ * captions-only fallback: English narration over foreign captions would be
609
+ * worse than silence.
610
+ */
611
+ declare function voiceFor(lang: string, gender?: VoiceGender): string | null;
612
+
613
+ export { getTierByEmail as $, type AIError as A, type VoiceGender as B, type CaptionStyle as C, DEFAULT_CAPTIONS as D, EXCLUDED_LINK_PATTERNS as E, applyProfile as F, ariaLabelSelector as G, type HighlightMode as H, INTERACTIVE_SELECTORS as I, buildSystemPrompt as J, buildUserPrompt as K, callAI as L, callAIViaRelay as M, NO_CLIP_OUTRO as N, crawlPage as O, PLAN_MAX_TOKENS as P, deStock as Q, RATE_FACTOR as R, SERVER_BACKOFF_MS as S, type Tier as T, type UserTier as U, type VoiceRate as V, defaultCaptionsFor as W, getErrorMessage as X, getLanguageName as Y, getStatus as Z, getStatusCached as _, type CaptionPosition as a, gotoSettled as a0, hasTrialRemaining as a1, hasVoice as a2, idSelector as a3, internationalVoiceFor as a4, isCanceling as a5, isExcludedLink as a6, isPaid as a7, isVerified as a8, normalizeUrl as a9, parseAIPlanResponse as aa, parsePartial as ab, pricingUrl as ac, rgbaToHex as ad, runPooled as ae, shouldSkipLink as af, showPaywall as ag, stripLongDashes as ah, toTitleCase as ai, voiceFor as aj, waitForPlaceholdersGone as ak, getTier as al, type Config as b, type AIResponse as c, type AIResult as d, CLI_BACKOFF_MS as e, CONFIG_FILE as f, type CaptionPreset as g, type CaptionWordStyle as h, type ChatOptions as i, DEFAULT_CONFIG as j, type ElementInfo as k, type ElementType as l, INTERNATIONAL_VOICE as m, type Plan as n, type RateLimitInfo as o, type RetryNotice as p, SIZE_PRESETS as q, type SparkStatus as r, type SparkSubscription as s, type SparkTrial as t, type Step as u, type SuggestedAction as v, VERSION as w, VIDEO_PROFILES as x, type VideoProfileName as y, type VideoSize as z };
package/dist/web.d.mts CHANGED
@@ -1,6 +1,7 @@
1
- import { P as Plan } from './scraping-C334VxX5.mjs';
2
- export { A as AIError, a as AIResponse, b as AIResult, E as EXCLUDED_LINK_PATTERNS, i as ElementInfo, j as ElementType, F as FEATURES, H as HighlightMode, I as INTERACTIVE_SELECTORS, R as RateLimitInfo, l as SparkStatus, m as SparkSubscription, n as SparkTrial, o as Step, p as SuggestedAction, T as Tier, q as buildSystemPrompt, r as buildUserPrompt, s as callAI, u as crawlPage, v as getErrorMessage, w as getLanguageName, x as getStatus, y as getStatusCached, a0 as getTier, z as getTierByEmail, B as hasAccess, G as hasTrialRemaining, K as isCanceling, L as isExcludedLink, M as isPaid, N as isVerified, O as normalizeUrl, Q as parseAIPlanResponse, W as parsePartial, X as pricingUrl, Y as rgbaToHex, Z as shouldSkipLink, _ as showPaywall, $ as toTitleCase } from './scraping-C334VxX5.mjs';
3
- export { WIDE_VOICES as LANGUAGE_VOICES, hexToAss as hexToASS, windowsDrivePathToWsl as windowsPathToWSL } from '@spark-apps/video-kit';
1
+ import { n as Plan } from './voices-CPpnWn39.mjs';
2
+ export { A as AIError, c as AIResponse, d as AIResult, i as ChatOptions, E as EXCLUDED_LINK_PATTERNS, k as ElementInfo, l as ElementType, H as HighlightMode, I as INTERACTIVE_SELECTORS, m as INTERNATIONAL_VOICE, P as PLAN_MAX_TOKENS, o as RateLimitInfo, r as SparkStatus, s as SparkSubscription, t as SparkTrial, u as Step, v as SuggestedAction, T as Tier, J as buildSystemPrompt, K as buildUserPrompt, L as callAI, O as crawlPage, Q as deStock, X as getErrorMessage, Y as getLanguageName, Z as getStatus, _ as getStatusCached, al as getTier, $ as getTierByEmail, a1 as hasTrialRemaining, a2 as hasVoice, a4 as internationalVoiceFor, a5 as isCanceling, a6 as isExcludedLink, a7 as isPaid, a8 as isVerified, a9 as normalizeUrl, aa as parseAIPlanResponse, ab as parsePartial, ac as pricingUrl, ad as rgbaToHex, ae as runPooled, af as shouldSkipLink, ag as showPaywall, ah as stripLongDashes, ai as toTitleCase, aj as voiceFor } from './voices-CPpnWn39.mjs';
3
+ export { MALE_VOICES, MULTILINGUAL_VOICES, WIDE_VOICES, hexToAss as hexToASS, windowsDrivePathToWsl as windowsPathToWSL } from '@spark-apps/video-kit';
4
+ import 'playwright';
4
5
 
5
6
  /**
6
7
  * The plan file, and handing it to the browser editor.
@@ -112,6 +113,7 @@ interface PreservedValues {
112
113
  mediaType: string | undefined;
113
114
  }>;
114
115
  mediaSteps: Array<{
116
+ value: string | undefined;
115
117
  mediaFile: string | undefined;
116
118
  mediaType: string | undefined;
117
119
  target: string | undefined;