agentreel 0.2.6 → 0.3.1

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.
Files changed (2) hide show
  1. package/bin/agentreel.mjs +75 -35
  2. package/package.json +1 -1
package/bin/agentreel.mjs CHANGED
@@ -158,11 +158,18 @@ function extractBrowserHighlights(videoPath, task) {
158
158
 
159
159
  function buildBrowserHighlights(clicks, videoPath, task) {
160
160
  const CLIP_DUR = 7;
161
+ const MIN_HIGHLIGHTS = 3;
162
+ const MAX_HIGHLIGHTS = 4;
161
163
  const labels = ["Overview", "Interact", "Navigate", "Result"];
162
164
  const overlays = ["**First look**", "**Key action**", "**Exploring**", "**The result**"];
163
165
 
164
- // If we have clicks, cluster them into highlights
165
- if (clicks.length >= 2) {
166
+ // Estimate video duration from last click or default to 25s
167
+ const lastClickTime = clicks.length > 0 ? clicks[clicks.length - 1].timeSec : 0;
168
+ const videoDur = Math.max(25, lastClickTime + 5);
169
+
170
+ // Build click-based highlights
171
+ const clickHighlights = [];
172
+ if (clicks.length >= 1) {
166
173
  // Group clicks that are within 3s of each other
167
174
  const clusters = [];
168
175
  let cluster = [clicks[0]];
@@ -177,14 +184,14 @@ function buildBrowserHighlights(clicks, videoPath, task) {
177
184
  }
178
185
  clusters.push(cluster);
179
186
 
180
- // Take up to 4 clusters, pick the ones with most clicks
187
+ // Take top clusters by density, sorted by time
181
188
  const ranked = clusters
182
- .map((c, i) => ({ cluster: c, idx: i }))
189
+ .map((c) => ({ cluster: c }))
183
190
  .sort((a, b) => b.cluster.length - a.cluster.length)
184
- .slice(0, 4)
191
+ .slice(0, MAX_HIGHLIGHTS)
185
192
  .sort((a, b) => a.cluster[0].timeSec - b.cluster[0].timeSec);
186
193
 
187
- const highlights = ranked.map((r, i) => {
194
+ for (const r of ranked) {
188
195
  const first = r.cluster[0];
189
196
  const last = r.cluster[r.cluster.length - 1];
190
197
  const center = (first.timeSec + last.timeSec) / 2;
@@ -200,41 +207,50 @@ function buildBrowserHighlights(clicks, videoPath, task) {
200
207
  const focusX = hlClicks.reduce((s, c) => s + c.x, 0) / hlClicks.length / 1280;
201
208
  const focusY = hlClicks.reduce((s, c) => s + c.y, 0) / hlClicks.length / 800;
202
209
 
203
- return {
204
- label: labels[i % labels.length],
205
- overlay: overlays[i % overlays.length],
210
+ clickHighlights.push({
206
211
  videoSrc: "browser-demo.mp4",
207
212
  videoStartSec: Math.round(startSec * 10) / 10,
208
213
  videoEndSec: Math.round(endSec * 10) / 10,
209
214
  focusX,
210
215
  focusY,
211
216
  clicks: hlClicks,
212
- };
213
- });
214
-
215
- console.error(` ${highlights.length} highlights from ${clicks.length} clicks`);
216
- return highlights;
217
+ });
218
+ }
217
219
  }
218
220
 
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;
221
+ // Pad to MIN_HIGHLIGHTS with evenly-spaced filler clips
222
+ const highlights = [...clickHighlights];
223
+ if (highlights.length < MIN_HIGHLIGHTS) {
224
+ // Find time gaps not covered by existing highlights
225
+ const covered = highlights.map(h => [h.videoStartSec, h.videoEndSec]);
226
+ const fillerCount = MIN_HIGHLIGHTS - highlights.length;
227
+
228
+ // Divide full video into slots, pick uncovered ones
229
+ const slotDur = videoDur / (fillerCount + covered.length + 1);
230
+ for (let i = 0; i < fillerCount; i++) {
231
+ const candidate = slotDur * (i + 1);
232
+ // Skip if overlaps with existing highlight
233
+ const overlaps = covered.some(([s, e]) => candidate >= s && candidate <= e);
234
+ const startSec = overlaps
235
+ ? Math.max(0, videoDur - CLIP_DUR * (fillerCount - i))
236
+ : Math.max(0, candidate - CLIP_DUR / 2);
237
+ highlights.push({
238
+ videoSrc: "browser-demo.mp4",
239
+ videoStartSec: Math.round(startSec * 10) / 10,
240
+ videoEndSec: Math.round((startSec + CLIP_DUR) * 10) / 10,
241
+ });
226
242
  }
227
- } catch {
228
- // Claude failed, use defaults
229
243
  }
230
244
 
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
- ];
245
+ // Sort by start time and assign labels
246
+ highlights.sort((a, b) => a.videoStartSec - b.videoStartSec);
247
+ for (let i = 0; i < highlights.length; i++) {
248
+ highlights[i].label = labels[i % labels.length];
249
+ highlights[i].overlay = overlays[i % overlays.length];
250
+ }
251
+
252
+ console.error(` ${highlights.length} highlights (${clickHighlights.length} from clicks, ${highlights.length - clickHighlights.length} filler)`);
253
+ return highlights;
238
254
  }
239
255
 
240
256
  // ── Render ──────────────────────────────────────────────────
@@ -268,7 +284,7 @@ async function renderVideo(props, output, musicPath) {
268
284
  inputProps: props,
269
285
  onBrowserDownload: () => {
270
286
  console.error(" Downloading renderer (one-time, ~90MB)...");
271
- return () => {}; // suppress progress logs
287
+ return { onProgress: () => {} };
272
288
  },
273
289
  });
274
290
 
@@ -323,10 +339,11 @@ async function shareFlow(outputPath, title, prompt) {
323
339
  const shouldShare = await askYesNo("Share to Twitter? [Y/n] ");
324
340
  if (!shouldShare) return;
325
341
 
326
- // Use prompt for tweet text if available, otherwise title
327
- const tweetBody = prompt || title;
328
-
329
- const text = `${tweetBody}\n\nMade with agentreel`;
342
+ const name = title || "this";
343
+ const desc = prompt || "";
344
+ const text = desc
345
+ ? `Introducing ${name} — ${desc}\n\nMade with https://github.com/islo-labs/agentreel`
346
+ : `Introducing ${name}\n\nMade with https://github.com/islo-labs/agentreel`;
330
347
  const tweetText = encodeURIComponent(text);
331
348
  const intentURL = `https://twitter.com/intent/tweet?text=${tweetText}`;
332
349
 
@@ -341,6 +358,22 @@ async function shareFlow(outputPath, title, prompt) {
341
358
  }
342
359
  }
343
360
 
361
+ // ── Auto-describe ──────────────────────────────────────────
362
+
363
+ function autoDescribe(cmd, url) {
364
+ const target = cmd || url;
365
+ try {
366
+ const result = execFileSync("claude", [
367
+ "-p",
368
+ `Describe what this tool/app does in one short sentence (under 10 words). No quotes, no period. Just the description.\n\n${target}`,
369
+ "--output-format", "text",
370
+ ], { encoding: "utf-8", timeout: 30000, stdio: ["ignore", "pipe", "ignore"] });
371
+ const desc = result.trim();
372
+ if (desc && desc.length < 100) return desc;
373
+ } catch { /* fall through */ }
374
+ return cmd ? cmd.split(/\s+/).pop() : "Web app demo";
375
+ }
376
+
344
377
  // ── Main ────────────────────────────────────────────────────
345
378
 
346
379
  async function main() {
@@ -352,6 +385,13 @@ async function main() {
352
385
  let demoURL = flags.url;
353
386
  let prompt = flags.prompt;
354
387
 
388
+ // Auto-generate description if not provided
389
+ if (!prompt) {
390
+ console.error("Generating description...");
391
+ prompt = autoDescribe(demoCmd, demoURL);
392
+ console.error(` "${prompt}"`);
393
+ }
394
+
355
395
  if (!demoCmd && !demoURL) {
356
396
  console.error("Please provide --cmd or --url.\n");
357
397
  printUsage();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentreel",
3
- "version": "0.2.6",
3
+ "version": "0.3.1",
4
4
  "description": "Turn your web apps and CLIs into viral clips",
5
5
  "bin": {
6
6
  "agentreel": "./bin/agentreel.mjs"