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 +86 -22
- package/package.json +1 -1
- package/scripts/browser_demo.py +15 -2
- package/scripts/cli_demo.py +10 -1
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:
|
|
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
|
-
//
|
|
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
|
-
|
|
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
package/scripts/browser_demo.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/scripts/cli_demo.py
CHANGED
|
@@ -227,7 +227,16 @@ Return ONLY a JSON array of highlights. No markdown fences."""
|
|
|
227
227
|
text = part
|
|
228
228
|
break
|
|
229
229
|
|
|
230
|
-
|
|
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__":
|