@spark-apps/quickpeek 1.2.0 → 1.2.2

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/README.md CHANGED
@@ -1,204 +1,204 @@
1
- [license-link]: ./LICENSE
2
- [privacy-link]: ./PRIVACY.md
3
- [discord-link]: https://discord.gg/5skXfKRytR
4
- [stars-link]: ../../stargazers
5
- [issues-link]: ../../issues
6
-
7
- <div align="center">
8
-
9
- <img src=imgs/logo.png>
10
-
11
- **Demo videos for your web app with AI**
12
-
13
- *Because Tutorial Videos are Overrated!*
14
-
15
- [![npm](https://img.shields.io/npm/v/@spark-apps/quickpeek?style=flat-square&logo=npm&logoColor=white&color=crimson)](https://www.npmjs.com/package/@spark-apps/quickpeek)
16
- [![MIT](https://img.shields.io/badge/License-MIT-blueviolet?style=flat-square)][license-link]
17
- [![Discord](https://img.shields.io/badge/Discord-Join-5865F2?style=flat-square&logo=discord&logoColor=white)][discord-link]
18
- [![GitHub Stars](https://img.shields.io/github/stars/muammar-yacoob/quickpeek?style=flat-square)][stars-link]
19
-
20
- </div>
21
-
22
- ---
23
-
24
- ## Quick Start
25
-
26
- ```bash
27
- npm install -g @spark-apps/quickpeek
28
- qp localhost:3000
29
- ```
30
-
31
- **Requirements:** [ffmpeg](https://ffmpeg.org/download.html), [Groq API key](https://console.groq.com/keys) (free)
32
-
33
- ## Features
34
-
35
- | Feature | Description |
36
- |---------|-------------|
37
- | **AI Planning** | Groq AI analyzes your app and generates demo steps |
38
- | **Script Mode** | Provide your own narration script, AI maps it to UI actions |
39
- | **Capture Mode** | Record your own interactions → replayable plan.json |
40
- | **Voice Over** | Free Edge TTS narration in 60+ languages |
41
- | **Karaoke Captions** | Word-by-word highlighted subtitles |
42
- | **Background Music** | Optional music with fade in/out |
43
- | **Smart Zoom** | Per-step zoom for focused demos |
44
- | **Chrome Extensions** | Record demos for Chrome extensions |
45
-
46
- ## Examples
47
-
48
- ### Basic — AI generates the plan
49
-
50
- ```bash
51
- qp localhost:3000
52
- ```
53
-
54
- ### Script mode — you write the narration
55
-
56
- ```bash
57
- qp localhost:3000 --script narration.txt
58
- ```
59
-
60
- Script file format (one caption per line, `#` for comments):
61
-
62
- ```
63
- # My app demo
64
- We start on the dashboard — your main workspace.
65
- Let's search for a customer by typing their name.
66
- Click the result to open their profile.
67
- ```
68
-
69
- ### Capture mode — record your own interactions
70
-
71
- ```bash
72
- qp capture localhost:3000
73
- ```
74
-
75
- Browse the app freely, press Enter when done. QuickPeek converts your clicks, typing, and scrolls into a `plan.json` that can be replayed as a polished video.
76
-
77
- ### With options
78
-
79
- ```bash
80
- qp localhost:3000 lang=es -d "A todo app for managing daily tasks"
81
- ```
82
-
83
- ### Chrome Extensions
84
-
85
- Record demos for Chrome extensions using the `chrome:` prefix:
86
-
87
- ```bash
88
- qp chrome:your-extension-id
89
- ```
90
-
91
- QuickPeek will automatically:
92
- 1. Find the extension in your Chrome installation
93
- 2. Load and analyze the extension popup UI
94
- 3. Generate a demo plan with AI
95
-
96
- The popup URL is detected from the extension's manifest.
97
-
98
- | Option | Description |
99
- |--------|-------------|
100
- | `lang` | Voice language (en, es, ar, zh...) |
101
- | `-d, --description` | App description to guide AI |
102
- | `-s, --script` | Path to narration script file |
103
- | `video.width/height` | Resolution (default: 1080x1920) |
104
- | `video.quality` | Quality 0-1 (default: 0.5) |
105
- | `music.path` | Background music file |
106
- | `music.volume` | Music volume 0-1 (default: 0.3) |
107
- | `output.dir` | Output directory (default: ./demo) |
108
-
109
- Config is auto-generated on first run. Edit `quickpeek.config.json` to customize.
110
-
111
- <details>
112
- <summary><strong>Supported Languages</strong></summary>
113
-
114
- | Code | Language | Code | Language |
115
- |------|----------|------|----------|
116
- | `en` | English | `ar` | Arabic |
117
- | `es` | Spanish | `fr` | French |
118
- | `de` | German | `zh` | Chinese |
119
- | `ja` | Japanese | `ko` | Korean |
120
- | `pt` | Portuguese | `hi` | Hindi |
121
- | `it` | Italian | `ru` | Russian |
122
-
123
- See `src/voices.ts` for all 60+ supported languages.
124
-
125
- </details>
126
-
127
- <details>
128
- <summary><strong>Caption Style</strong></summary>
129
-
130
- Edit `captions.css` (auto-generated) to customize:
131
-
132
- ![Classic Yellow](imgs/caption-classic.png)
133
-
134
- ```css
135
- .caption {
136
- font-family: Arial, sans-serif;
137
- -webkit-text-stroke: 3px #000000;
138
- }
139
- .caption .regular {
140
- font-size: 56px;
141
- color: #FFFFFF;
142
- }
143
- .caption .highlight {
144
- font-size: 64px;
145
- color: #FFFF00;
146
- }
147
- ```
148
-
149
- </details>
150
-
151
- ## Plan Actions
152
-
153
- Edit `demo/plan.json` to customize your demo. Each step uses one of these actions:
154
-
155
- | Action | Target | Value | Description |
156
- |--------|--------|-------|-------------|
157
- | `wait` | - | - | Pause for narration (intro/outro) |
158
- | `click` | `"button text"` | - | Click button/link |
159
- | `click-N` | `"button text"` | - | Click N times (e.g., `click-2`, `click-3`) |
160
- | `type` | `input[name="field"]` | `"text to type"` | Type into input field |
161
- | `upload` | `input[type="file"]` | `"path/to/file"` | Upload file to form |
162
- | `select` | `select[name="field"]` | `"option text"` | Select dropdown option |
163
- | `check` | `input[type="checkbox"]` | - | Toggle checkbox |
164
- | `drag` | `input[type="range"]` | `"0"`-`"100"` | Drag slider to value |
165
- | `drag` | `canvas` | `"around"` | Full 360° rotation (returns to start) |
166
- | `drag` | `canvas` | `"rotate"` | Rotate ~270° (stays at new angle) |
167
- | `drag` | `canvas` | `"rotate:90"` | Rotate specific angle (circular) |
168
- | `drag` | `canvas` | `"rotate:x-90"` | Rotate on X axis (horizontal drag) |
169
- | `drag` | `canvas` | `"rotate:y45"` | Rotate on Y axis (vertical drag) |
170
- | `drag` | - | `"left"` `"right"` `"up"` `"down"` | Directional drag |
171
- | `scroll` | - | `"up"` `"down"` `"bottom"` | Scroll page |
172
- | `scroll` | - | `"-10"` to `"10"` | Zoom camera (negative=out, positive=in) |
173
- | `media` | `"scale"` or `"center"` | `"path/to/file"` | Display image/video fullscreen |
174
- | `showDownload` | - | - | Show the last downloaded file |
175
-
176
- **Target shortcuts:** For buttons and links, use simplified text like `"download"` or `"extract frame"` instead of CSS selectors.
177
-
178
- ## Output
179
-
180
- | File | Description |
181
- |------|-------------|
182
- | `demo/demo.mp4` | Final video |
183
- | `demo/plan.json` | Editable script (re-run to apply changes) |
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
-
189
- ## 🌱 Support & Contributions
190
-
191
- ⭐ **Star the repo** & I power up like Mario 🍄
192
- ☕ **Devs run on coffee** - [Buy me one?][coffee-link]
193
- 💖 **Sponsor** [Your support][stars-link] helps maintain and improve the tool<br>
194
- 💰 **Crypto tips welcome** - [Tip in crypto](https://tip.md/muammar-yacoob)
195
- 🤝 **Contributions are welcome** - [🍴 Fork][fork-link], improve, PR!
196
- 🎥 **Need help?** <img src="https://img.icons8.com/color/20/youtube-play.png" alt="YouTube" width="20" height="20" style="vertical-align: middle;"> [Setup Tutorial][vid-link] • <img src="https://img.icons8.com/color/20/discord--v2.png" alt="Discord" width="20" height="20" style="vertical-align: middle;"> [Join Discord][discord-link]
197
-
198
- <div align="center">
199
-
200
- ---
201
-
202
- MIT License - Made with ☕ by [Spark Apps](https://github.com/muammar-yacoob)
203
-
204
- </div>
1
+ [license-link]: ./LICENSE
2
+ [privacy-link]: ./PRIVACY.md
3
+ [discord-link]: https://discord.gg/mAjHWE3mSp
4
+ [stars-link]: ../../stargazers
5
+ [issues-link]: ../../issues
6
+
7
+ <div align="center">
8
+
9
+ <img src=imgs/logo.png>
10
+
11
+ **Demo videos for your web app with AI**
12
+
13
+ *Because Tutorial Videos are Overrated!*
14
+
15
+ [![npm](https://img.shields.io/npm/v/@spark-apps/quickpeek?style=flat-square&logo=npm&logoColor=white&color=crimson)](https://www.npmjs.com/package/@spark-apps/quickpeek)
16
+ [![MIT](https://img.shields.io/badge/License-MIT-blueviolet?style=flat-square)][license-link]
17
+ [![Discord](https://img.shields.io/badge/Discord-Join-5865F2?style=flat-square&logo=discord&logoColor=white)][discord-link]
18
+ [![GitHub Stars](https://img.shields.io/github/stars/muammar-yacoob/quickpeek?style=flat-square)][stars-link]
19
+
20
+ </div>
21
+
22
+ ---
23
+
24
+ ## Quick Start
25
+
26
+ ```bash
27
+ npm install -g @spark-apps/quickpeek
28
+ qp localhost:3000
29
+ ```
30
+
31
+ **Requirements:** [ffmpeg](https://ffmpeg.org/download.html), [Groq API key](https://console.groq.com/keys) (free)
32
+
33
+ ## Features
34
+
35
+ | Feature | Description |
36
+ |---------|-------------|
37
+ | **AI Planning** | Groq AI analyzes your app and generates demo steps |
38
+ | **Script Mode** | Provide your own narration script, AI maps it to UI actions |
39
+ | **Capture Mode** | Record your own interactions → replayable plan.json |
40
+ | **Voice Over** | Free Edge TTS narration in 60+ languages |
41
+ | **Karaoke Captions** | Word-by-word highlighted subtitles |
42
+ | **Background Music** | Optional music with fade in/out |
43
+ | **Smart Zoom** | Per-step zoom for focused demos |
44
+ | **Chrome Extensions** | Record demos for Chrome extensions |
45
+
46
+ ## Examples
47
+
48
+ ### Basic — AI generates the plan
49
+
50
+ ```bash
51
+ qp localhost:3000
52
+ ```
53
+
54
+ ### Script mode — you write the narration
55
+
56
+ ```bash
57
+ qp localhost:3000 --script narration.txt
58
+ ```
59
+
60
+ Script file format (one caption per line, `#` for comments):
61
+
62
+ ```
63
+ # My app demo
64
+ We start on the dashboard — your main workspace.
65
+ Let's search for a customer by typing their name.
66
+ Click the result to open their profile.
67
+ ```
68
+
69
+ ### Capture mode — record your own interactions
70
+
71
+ ```bash
72
+ qp capture localhost:3000
73
+ ```
74
+
75
+ Browse the app freely, press Enter when done. QuickPeek converts your clicks, typing, and scrolls into a `plan.json` that can be replayed as a polished video.
76
+
77
+ ### With options
78
+
79
+ ```bash
80
+ qp localhost:3000 lang=es -d "A todo app for managing daily tasks"
81
+ ```
82
+
83
+ ### Chrome Extensions
84
+
85
+ Record demos for Chrome extensions using the `chrome:` prefix:
86
+
87
+ ```bash
88
+ qp chrome:your-extension-id
89
+ ```
90
+
91
+ QuickPeek will automatically:
92
+ 1. Find the extension in your Chrome installation
93
+ 2. Load and analyze the extension popup UI
94
+ 3. Generate a demo plan with AI
95
+
96
+ The popup URL is detected from the extension's manifest.
97
+
98
+ | Option | Description |
99
+ |--------|-------------|
100
+ | `lang` | Voice language (en, es, ar, zh...) |
101
+ | `-d, --description` | App description to guide AI |
102
+ | `-s, --script` | Path to narration script file |
103
+ | `video.width/height` | Resolution (default: 1080x1920) |
104
+ | `video.quality` | Quality 0-1 (default: 0.5) |
105
+ | `music.path` | Background music file |
106
+ | `music.volume` | Music volume 0-1 (default: 0.3) |
107
+ | `output.dir` | Output directory (default: ./demo) |
108
+
109
+ Config is auto-generated on first run. Edit `quickpeek.config.json` to customize.
110
+
111
+ <details>
112
+ <summary><strong>Supported Languages</strong></summary>
113
+
114
+ | Code | Language | Code | Language |
115
+ |------|----------|------|----------|
116
+ | `en` | English | `ar` | Arabic |
117
+ | `es` | Spanish | `fr` | French |
118
+ | `de` | German | `zh` | Chinese |
119
+ | `ja` | Japanese | `ko` | Korean |
120
+ | `pt` | Portuguese | `hi` | Hindi |
121
+ | `it` | Italian | `ru` | Russian |
122
+
123
+ See `src/voices.ts` for all 60+ supported languages.
124
+
125
+ </details>
126
+
127
+ <details>
128
+ <summary><strong>Caption Style</strong></summary>
129
+
130
+ Edit `captions.css` (auto-generated) to customize:
131
+
132
+ ![Classic Yellow](imgs/caption-classic.png)
133
+
134
+ ```css
135
+ .caption {
136
+ font-family: Arial, sans-serif;
137
+ -webkit-text-stroke: 3px #000000;
138
+ }
139
+ .caption .regular {
140
+ font-size: 56px;
141
+ color: #FFFFFF;
142
+ }
143
+ .caption .highlight {
144
+ font-size: 64px;
145
+ color: #FFFF00;
146
+ }
147
+ ```
148
+
149
+ </details>
150
+
151
+ ## Plan Actions
152
+
153
+ Edit `demo/plan.json` to customize your demo. Each step uses one of these actions:
154
+
155
+ | Action | Target | Value | Description |
156
+ |--------|--------|-------|-------------|
157
+ | `wait` | - | - | Pause for narration (intro/outro) |
158
+ | `click` | `"button text"` | - | Click button/link |
159
+ | `click-N` | `"button text"` | - | Click N times (e.g., `click-2`, `click-3`) |
160
+ | `type` | `input[name="field"]` | `"text to type"` | Type into input field |
161
+ | `upload` | `input[type="file"]` | `"path/to/file"` | Upload file to form |
162
+ | `select` | `select[name="field"]` | `"option text"` | Select dropdown option |
163
+ | `check` | `input[type="checkbox"]` | - | Toggle checkbox |
164
+ | `drag` | `input[type="range"]` | `"0"`-`"100"` | Drag slider to value |
165
+ | `drag` | `canvas` | `"around"` | Full 360° rotation (returns to start) |
166
+ | `drag` | `canvas` | `"rotate"` | Rotate ~270° (stays at new angle) |
167
+ | `drag` | `canvas` | `"rotate:90"` | Rotate specific angle (circular) |
168
+ | `drag` | `canvas` | `"rotate:x-90"` | Rotate on X axis (horizontal drag) |
169
+ | `drag` | `canvas` | `"rotate:y45"` | Rotate on Y axis (vertical drag) |
170
+ | `drag` | - | `"left"` `"right"` `"up"` `"down"` | Directional drag |
171
+ | `scroll` | - | `"up"` `"down"` `"bottom"` | Scroll page |
172
+ | `scroll` | - | `"-10"` to `"10"` | Zoom camera (negative=out, positive=in) |
173
+ | `media` | `"scale"` or `"center"` | `"path/to/file"` | Display image/video fullscreen |
174
+ | `showDownload` | - | - | Show the last downloaded file |
175
+
176
+ **Target shortcuts:** For buttons and links, use simplified text like `"download"` or `"extract frame"` instead of CSS selectors.
177
+
178
+ ## Output
179
+
180
+ | File | Description |
181
+ |------|-------------|
182
+ | `demo/demo.mp4` | Final video |
183
+ | `demo/plan.json` | Editable script (re-run to apply changes) |
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
+
189
+ ## 🌱 Support & Contributions
190
+
191
+ ⭐ **Star the repo** & I power up like Mario 🍄
192
+ ☕ **Devs run on coffee** - [Buy me one?][coffee-link]
193
+ 💖 **Sponsor** [Your support][stars-link] helps maintain and improve the tool<br>
194
+ 💰 **Crypto tips welcome** - [Tip in crypto](https://tip.md/muammar-yacoob)
195
+ 🤝 **Contributions are welcome** - [🍴 Fork][fork-link], improve, PR!
196
+ 🎥 **Need help?** <img src="https://img.icons8.com/color/20/youtube-play.png" alt="YouTube" width="20" height="20" style="vertical-align: middle;"> [Setup Tutorial][vid-link] • <img src="https://img.icons8.com/color/20/discord--v2.png" alt="Discord" width="20" height="20" style="vertical-align: middle;"> [Join Discord][discord-link]
197
+
198
+ <div align="center">
199
+
200
+ ---
201
+
202
+ MIT License - Made with ☕ by [Spark Apps](https://github.com/muammar-yacoob)
203
+
204
+ </div>
@@ -1,15 +1,15 @@
1
- #qp-highlight-overlay {
2
- position: fixed;
3
- inset: 0;
4
- background: rgba(0, 0, 0, 0.5);
5
- z-index: 999998;
6
- pointer-events: none;
7
- }
8
-
9
- [data-qp-highlight] {
10
- position: relative !important;
11
- z-index: 999999 !important;
12
- outline: 3px solid #ffd700 !important;
13
- outline-offset: 2px !important;
14
- border-radius: 8px !important;
15
- }
1
+ #qp-highlight-overlay {
2
+ position: fixed;
3
+ inset: 0;
4
+ background: rgba(0, 0, 0, 0.5);
5
+ z-index: 999998;
6
+ pointer-events: none;
7
+ }
8
+
9
+ [data-qp-highlight] {
10
+ position: relative !important;
11
+ z-index: 999999 !important;
12
+ outline: 3px solid #ffd700 !important;
13
+ outline-offset: 2px !important;
14
+ border-radius: 8px !important;
15
+ }
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
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';
1
+ import { U as UserTier, C as Config } from './scraping-BgepklP3.mjs';
2
+ export { A as AIError, a as AIResponse, b as AIResult, c as CLI_BACKOFF_MS, d as CONFIG_FILE, e as CaptionStyle, f as CaptionWordStyle, g as ChatOptions, D as DEFAULT_CAPTIONS, h as DEFAULT_CONFIG, E as EXCLUDED_LINK_PATTERNS, i as ElementInfo, j as ElementType, F as FEATURES, H as HighlightMode, I as INTERACTIVE_SELECTORS, P as Plan, R as RateLimitInfo, k as RetryNotice, S as SERVER_BACKOFF_MS, l as SparkStatus, m as SparkSubscription, n as SparkTrial, o as Step, p as SuggestedAction, T as Tier, V as VERSION, q as buildSystemPrompt, r as buildUserPrompt, s as callAI, t as callAIViaRelay, u as crawlPage, v as getErrorMessage, w as getLanguageName, x as getStatus, y as getStatusCached, z as getTierByEmail, B as hasAccess, G as hasTrialRemaining, J as idSelector, 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-BgepklP3.mjs';
3
3
  export { WIDE_VOICES as LANGUAGE_VOICES, hexToAss as hexToASS, windowsDrivePathToWsl as windowsPathToWSL } from '@spark-apps/video-kit';
4
4
 
5
5
  /**
@@ -124,9 +124,27 @@ declare function mapTimestampsAfterCompression(originalTimestampsSeconds: number
124
124
  * Run ffprobe to get media duration in milliseconds
125
125
  */
126
126
  declare function getMediaDuration(filePath: string): Promise<number>;
127
+ /**
128
+ * Report the codec of a file's first audio stream, or '' if it cannot be read
129
+ */
130
+ declare function getAudioCodec(filePath: string): Promise<string>;
131
+ /**
132
+ * A WAV whose stream is not PCM was written by the old stream-copy concat and
133
+ * misreports its duration, so it must never be reused - see concatMedia.
134
+ */
135
+ declare function isHonestWav(filePath: string): Promise<boolean>;
127
136
  /**
128
137
  * Concatenate audio files using ffmpeg concat demuxer
138
+ *
139
+ * The segments are MP3 (edge-tts clips and generated silence). Stream-copying
140
+ * them into a WAV container stores MP3 packets behind a PCM-style header, so
141
+ * every later ffprobe derives the duration from the PCM byte rate and reports
142
+ * a wildly inflated length - a 39.7s voiceover probed as 57.6s. That bogus
143
+ * figure then drives step alignment and the compose filters, which is why the
144
+ * voiceover ran far past the end of the video. Decode to real PCM whenever the
145
+ * output is WAV so the duration is honest; other containers can still copy.
129
146
  */
147
+ declare function buildConcatArgs(listPath: string, output: string): string[];
130
148
  declare function concatMedia(listPath: string, output: string): Promise<void>;
131
149
  /**
132
150
  * Generate silence audio file of specified duration
@@ -172,6 +190,64 @@ declare function buildStepTransitions(steps: {
172
190
  */
173
191
  declare function applyTransitions(opts: ApplyTransitionsOptions): Promise<void>;
174
192
 
193
+ /**
194
+ * Deterministic repair for JSON that a model emitted imperfectly.
195
+ *
196
+ * Three failure modes show up in practice, in rising order of cost to recover:
197
+ *
198
+ * 1. Bad \u escapes - emoji written as \u{1F525} or a clipped \uD83.
199
+ * 2. Truncation - the response hit the upstream output-token ceiling mid-array,
200
+ * so the JSON simply stops. This is the one that used to surface as an opaque
201
+ * "JSON Parse error: Expected ']'", because the greedy \{[\s\S]*\} extraction
202
+ * ends at some inner object's closing brace.
203
+ * 3. Genuine malformation - a stray comma, an unescaped quote in a caption.
204
+ *
205
+ * (1) and (2) are recoverable here without a network round trip. Only (3) needs
206
+ * the model, so callers reach for AI repair (see ai/json-plan.ts) after this.
207
+ */
208
+ /**
209
+ * Repair invalid \u escapes that JSON.parse rejects outright.
210
+ *
211
+ * Captions routinely contain emoji, and models emit them as the ES6 form
212
+ * \u{1F525} or as a truncated \uD83 rather than a bare character or a proper
213
+ * surrogate pair. Both are "Bad Unicode escape" to JSON.parse. Asking the model
214
+ * to fix its own output does not help here - it re-emits the same escape - so
215
+ * normalise deterministically before spending a round trip.
216
+ */
217
+ declare function repairUnicodeEscapes(content: string): string;
218
+ /**
219
+ * Recover the longest valid prefix of a truncated JSON document.
220
+ *
221
+ * Walks the text once recording every position that sits immediately after a
222
+ * *complete* value, along with the bracket stack open at that point. Then works
223
+ * backwards from the end, cutting at each candidate and appending the closers
224
+ * that stack needs. The first prefix that parses wins, which for a truncated
225
+ * steps array means every whole step survives and only the half-written one is
226
+ * dropped.
227
+ *
228
+ * Returns null when nothing parses - a document too mangled to be truncation.
229
+ */
230
+ declare function salvageTruncatedJson(content: string): string | null;
231
+ /**
232
+ * Pull the JSON payload out of a model response that may be fenced, prefixed
233
+ * with prose, or cut off before its closing bracket.
234
+ *
235
+ * Yields candidates rather than one string: the trim-to-last-closer variant is
236
+ * right when the model appended prose, and the run-to-end variant is right when
237
+ * the response was truncated (the last closer is then some inner object's).
238
+ */
239
+ declare function extractJsonCandidates(content: string): string[];
240
+ interface LenientParse<T> {
241
+ value: T;
242
+ /** True when the text needed repair, i.e. worth telling the user about. */
243
+ repaired: boolean;
244
+ }
245
+ /**
246
+ * Parse JSON out of a model response, repairing what can be repaired without
247
+ * calling back out to the model. Returns null if nothing salvageable is there.
248
+ */
249
+ declare function parseAIJson<T>(content: string): LenientParse<T> | null;
250
+
175
251
  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
252
  /**
177
253
  * JavaScript to inject for cursor tracking
@@ -303,4 +379,4 @@ declare function buildWatermarkFilters(opts: {
303
379
  videoDuration: number;
304
380
  }): string;
305
381
 
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 };
382
+ export { type ApplyTransitionsOptions, type AudioResult, type BlendTransition, CURSOR_CSS, CURSOR_PNG, CURSOR_SCRIPT, type ComposeOptions, type ConcatVideoOptions, Config, type LenientParse, type SpeedUpOptions, type SpliceVideosOptions, type StepAudio, type StepSync, type StepTransition, type TTSStep, type TimeSegment, UserTier, type VideoOverlay, type VoiceoverMeta, type WordTiming, alignVoiceoverToRecording, applyTransitions, buildConcatArgs, buildKeepSegments, buildStepTransitions, buildWatermarkFilters, canReuseVoiceover, composeVideo, concatMedia, concatVideoWithOutro, estimateWordTimings, extractJsonCandidates, generateASSHeader, generateSilence, generateVoiceover, getAudioCodec, getMediaDuration, getUpgradeUrl, isHonestWav, loadExistingVoiceover, loadVoiceoverMeta, mapTimestampsAfterCompression, parseAIJson, registerFreeUser, repairUnicodeEscapes, resolveVoice, salvageTruncatedJson, shouldWatermark, speedUpVideo, spliceVideos, synthesizeSpeech };