agentreel 0.2.7 → 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.
- package/bin/agentreel.mjs +74 -34
- 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
|
-
//
|
|
165
|
-
|
|
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
|
|
187
|
+
// Take top clusters by density, sorted by time
|
|
181
188
|
const ranked = clusters
|
|
182
|
-
.map((c
|
|
189
|
+
.map((c) => ({ cluster: c }))
|
|
183
190
|
.sort((a, b) => b.cluster.length - a.cluster.length)
|
|
184
|
-
.slice(0,
|
|
191
|
+
.slice(0, MAX_HIGHLIGHTS)
|
|
185
192
|
.sort((a, b) => a.cluster[0].timeSec - b.cluster[0].timeSec);
|
|
186
193
|
|
|
187
|
-
const
|
|
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
|
-
|
|
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
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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 ──────────────────────────────────────────────────
|
|
@@ -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
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
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();
|