agentreel 0.2.4 → 0.2.6

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/bin/agentreel.mjs CHANGED
@@ -154,6 +154,89 @@ function extractBrowserHighlights(videoPath, task) {
154
154
  return outFile;
155
155
  }
156
156
 
157
+ // ── Browser Highlight Builder ───────────────────────────────
158
+
159
+ function buildBrowserHighlights(clicks, videoPath, task) {
160
+ const CLIP_DUR = 7;
161
+ const labels = ["Overview", "Interact", "Navigate", "Result"];
162
+ const overlays = ["**First look**", "**Key action**", "**Exploring**", "**The result**"];
163
+
164
+ // If we have clicks, cluster them into highlights
165
+ if (clicks.length >= 2) {
166
+ // Group clicks that are within 3s of each other
167
+ const clusters = [];
168
+ let cluster = [clicks[0]];
169
+
170
+ for (let i = 1; i < clicks.length; i++) {
171
+ if (clicks[i].timeSec - cluster[cluster.length - 1].timeSec < 3) {
172
+ cluster.push(clicks[i]);
173
+ } else {
174
+ clusters.push(cluster);
175
+ cluster = [clicks[i]];
176
+ }
177
+ }
178
+ clusters.push(cluster);
179
+
180
+ // Take up to 4 clusters, pick the ones with most clicks
181
+ const ranked = clusters
182
+ .map((c, i) => ({ cluster: c, idx: i }))
183
+ .sort((a, b) => b.cluster.length - a.cluster.length)
184
+ .slice(0, 4)
185
+ .sort((a, b) => a.cluster[0].timeSec - b.cluster[0].timeSec);
186
+
187
+ const highlights = ranked.map((r, i) => {
188
+ const first = r.cluster[0];
189
+ const last = r.cluster[r.cluster.length - 1];
190
+ const center = (first.timeSec + last.timeSec) / 2;
191
+ const startSec = Math.max(0, center - CLIP_DUR / 2);
192
+ const endSec = startSec + CLIP_DUR;
193
+
194
+ const hlClicks = r.cluster.map(c => ({
195
+ x: Math.max(0, Math.min(1280, c.x)),
196
+ y: Math.max(0, Math.min(800, c.y)),
197
+ timeSec: c.timeSec - startSec,
198
+ }));
199
+
200
+ const focusX = hlClicks.reduce((s, c) => s + c.x, 0) / hlClicks.length / 1280;
201
+ const focusY = hlClicks.reduce((s, c) => s + c.y, 0) / hlClicks.length / 800;
202
+
203
+ return {
204
+ label: labels[i % labels.length],
205
+ overlay: overlays[i % overlays.length],
206
+ videoSrc: "browser-demo.mp4",
207
+ videoStartSec: Math.round(startSec * 10) / 10,
208
+ videoEndSec: Math.round(endSec * 10) / 10,
209
+ focusX,
210
+ focusY,
211
+ clicks: hlClicks,
212
+ };
213
+ });
214
+
215
+ console.error(` ${highlights.length} highlights from ${clicks.length} clicks`);
216
+ return highlights;
217
+ }
218
+
219
+ // Fallback: try Claude extraction, or use evenly-spaced defaults
220
+ try {
221
+ const highlightsPath = extractBrowserHighlights(videoPath, task);
222
+ const highlights = JSON.parse(readFileSync(highlightsPath, "utf-8"));
223
+ if (highlights.length > 0) {
224
+ console.error(` ${highlights.length} highlights from Claude`);
225
+ return highlights;
226
+ }
227
+ } catch {
228
+ // Claude failed, use defaults
229
+ }
230
+
231
+ // Last resort: evenly-spaced clips
232
+ console.error(" Using default highlights (no clicks, no Claude)");
233
+ return [
234
+ { label: "Overview", overlay: "**Quick look**", videoSrc: "browser-demo.mp4", videoStartSec: 1, videoEndSec: 8 },
235
+ { label: "Features", overlay: "**Key features**", videoSrc: "browser-demo.mp4", videoStartSec: 8, videoEndSec: 15 },
236
+ { label: "Result", overlay: "**See it work**", videoSrc: "browser-demo.mp4", videoStartSec: 15, videoEndSec: 22 },
237
+ ];
238
+ }
239
+
157
240
  // ── Render ──────────────────────────────────────────────────
158
241
 
159
242
  async function renderVideo(props, output, musicPath) {
@@ -312,12 +395,9 @@ async function main() {
312
395
  if (!existsSync(publicDir)) mkdirSync(publicDir, { recursive: true });
313
396
  copyFileSync(videoPath, join(publicDir, "browser-demo.mp4"));
314
397
 
315
- console.error("Step 2/3: Extracting highlights...");
316
- const highlightsPath = extractBrowserHighlights(videoPath, task);
317
- const highlights = JSON.parse(readFileSync(highlightsPath, "utf-8"));
318
- console.error(` ${highlights.length} highlights extracted`);
398
+ console.error("Step 2/3: Building highlights...");
319
399
 
320
- // Merge click data into highlights
400
+ // Read click data this is the primary signal for highlights
321
401
  const clicksPath = videoPath.replace(".mp4", "-clicks.json");
322
402
  let allClicks = [];
323
403
  if (existsSync(clicksPath)) {
@@ -325,23 +405,7 @@ async function main() {
325
405
  console.error(` ${allClicks.length} clicks captured`);
326
406
  }
327
407
 
328
- for (const h of highlights) {
329
- const startSec = h.videoStartSec || 0;
330
- const endSec = h.videoEndSec || (startSec + 7);
331
-
332
- h.clicks = allClicks
333
- .filter(c => c.timeSec >= startSec && c.timeSec <= endSec)
334
- .map(c => ({
335
- x: Math.max(0, Math.min(1280, c.x)),
336
- y: Math.max(0, Math.min(800, c.y)),
337
- timeSec: c.timeSec - startSec,
338
- }));
339
-
340
- if (h.clicks.length > 0) {
341
- h.focusX = h.clicks.reduce((s, c) => s + c.x, 0) / h.clicks.length / 1280;
342
- h.focusY = h.clicks.reduce((s, c) => s + c.y, 0) / h.clicks.length / 800;
343
- }
344
- }
408
+ const highlights = buildBrowserHighlights(allClicks, videoPath, task);
345
409
 
346
410
  console.error("Step 3/3: Rendering video...");
347
411
  await renderVideo({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentreel",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Turn your web apps and CLIs into viral clips",
5
5
  "bin": {
6
6
  "agentreel": "./bin/agentreel.mjs"
@@ -92,6 +92,12 @@ def extract_highlights(video_path, task):
92
92
  )
93
93
 
94
94
  text = result.stdout.strip()
95
+ if result.returncode != 0 or not text:
96
+ print(f"Claude returned no output (exit {result.returncode}), using default highlights", file=sys.stderr)
97
+ if result.stderr:
98
+ print(f" stderr: {result.stderr[:200]}", file=sys.stderr)
99
+ text = ""
100
+
95
101
  if "```" in text:
96
102
  parts = text.split("```")
97
103
  for part in parts:
@@ -102,9 +108,16 @@ def extract_highlights(video_path, task):
102
108
  text = part
103
109
  break
104
110
 
105
- highlights = json.loads(text)
111
+ try:
112
+ highlights = json.loads(text)
113
+ except (json.JSONDecodeError, ValueError):
114
+ print(f"Could not parse highlights, using defaults", file=sys.stderr)
115
+ highlights = [
116
+ {"label": "Overview", "overlay": "**Quick look**", "videoStartSec": 1, "videoEndSec": 7},
117
+ {"label": "Features", "overlay": "**Key features**", "videoStartSec": 7, "videoEndSec": 14},
118
+ {"label": "Result", "overlay": "**See it work**", "videoStartSec": 14, "videoEndSec": 20},
119
+ ]
106
120
 
107
- # Add videoSrc to each highlight (the Go/JS CLI will set the actual path)
108
121
  for h in highlights:
109
122
  h["videoSrc"] = "browser-demo.mp4"
110
123
 
@@ -227,7 +227,16 @@ Return ONLY a JSON array of highlights. No markdown fences."""
227
227
  text = part
228
228
  break
229
229
 
230
- return json.loads(text)
230
+ try:
231
+ return json.loads(text)
232
+ except (json.JSONDecodeError, ValueError):
233
+ print(f"Could not parse highlights, using defaults", file=sys.stderr)
234
+ return [
235
+ {"label": "Run", "lines": [
236
+ {"text": "Running...", "isPrompt": True},
237
+ {"text": " Done.", "color": "#50fa7b"},
238
+ ]},
239
+ ]
231
240
 
232
241
 
233
242
  if __name__ == "__main__":