clipwise 0.10.0 → 0.10.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/dist/cli/index.js +39 -20
- package/dist/index.js +38 -19
- package/package.json +1 -1
- package/skills/clipwise.md +4 -1
package/dist/cli/index.js
CHANGED
|
@@ -3978,6 +3978,7 @@ __export(runner_exports, {
|
|
|
3978
3978
|
});
|
|
3979
3979
|
import { chromium as chromium2 } from "playwright";
|
|
3980
3980
|
import { createServer } from "http";
|
|
3981
|
+
import { createHash } from "crypto";
|
|
3981
3982
|
import { execSync } from "child_process";
|
|
3982
3983
|
import { readFile as readFile4, writeFile as writeFile2, mkdtemp, rm as rm2 } from "fs/promises";
|
|
3983
3984
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -4119,21 +4120,21 @@ async function recordFootageTake(scenario, scene, selectors) {
|
|
|
4119
4120
|
const recorder = new ClipwiseRecorder();
|
|
4120
4121
|
const session = await recorder.record(takeScenario);
|
|
4121
4122
|
const renderer = new CanvasRenderer(takeScenario.effects, segmentOutput(scenario), scene.steps);
|
|
4122
|
-
const
|
|
4123
|
-
|
|
4124
|
-
const
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4123
|
+
const framesDir = await mkdtemp(join2(tmpdir2(), `clipwise-footage-${scene.id}-`));
|
|
4124
|
+
let count = 0;
|
|
4125
|
+
for await (const f of renderer.composeStream(session.frames)) {
|
|
4126
|
+
const png = f.rawInfo ? await sharp10(f.buffer, {
|
|
4127
|
+
raw: { width: f.rawInfo.width, height: f.rawInfo.height, channels: f.rawInfo.channels }
|
|
4128
|
+
}).png().toBuffer() : f.buffer;
|
|
4129
|
+
await writeFile2(join2(framesDir, `${count}.png`), png);
|
|
4130
|
+
count++;
|
|
4131
|
+
}
|
|
4131
4132
|
const anchors = [];
|
|
4132
4133
|
for (let k = 0; k < scene.steps.length; k++) {
|
|
4133
4134
|
const idx = session.frames.findIndex((f) => (f.stepIndex ?? 0) >= k);
|
|
4134
4135
|
anchors.push(Math.max(0, idx) / scenario.output.fps);
|
|
4135
4136
|
}
|
|
4136
|
-
return {
|
|
4137
|
+
return { framesDir, count, anchors, boxes };
|
|
4137
4138
|
}
|
|
4138
4139
|
function fitCardW(cw, ch, maxW = 940, maxH = 540) {
|
|
4139
4140
|
return Math.round(Math.min(maxW, maxH * cw / ch));
|
|
@@ -4193,7 +4194,7 @@ function vignetteProps(scene, take, serverBase, scenario, brand, durMs) {
|
|
|
4193
4194
|
label: scene.label ?? "",
|
|
4194
4195
|
caption: scene.caption ?? "",
|
|
4195
4196
|
base: serverBase,
|
|
4196
|
-
count: take.
|
|
4197
|
+
count: take.count,
|
|
4197
4198
|
fps: scenario.output.fps,
|
|
4198
4199
|
start,
|
|
4199
4200
|
rate: scene.rate,
|
|
@@ -4251,9 +4252,14 @@ async function renderScenesTimeline(scenario, scenarioDir, onProgress) {
|
|
|
4251
4252
|
const m = req.url?.match(/^\/([\w-]+)\/(\d+)\.png$/);
|
|
4252
4253
|
const take = m ? takes.get(m[1]) : void 0;
|
|
4253
4254
|
if (take) {
|
|
4254
|
-
const idx = Math.min(take.
|
|
4255
|
-
|
|
4256
|
-
|
|
4255
|
+
const idx = Math.min(take.count - 1, parseInt(m[2], 10));
|
|
4256
|
+
readFile4(join2(take.framesDir, `${idx}.png`)).then((png) => {
|
|
4257
|
+
res.writeHead(200, { "content-type": "image/png", "cache-control": "max-age=3600" });
|
|
4258
|
+
res.end(png);
|
|
4259
|
+
}).catch(() => {
|
|
4260
|
+
res.writeHead(404);
|
|
4261
|
+
res.end();
|
|
4262
|
+
});
|
|
4257
4263
|
} else {
|
|
4258
4264
|
res.writeHead(404);
|
|
4259
4265
|
res.end();
|
|
@@ -4311,24 +4317,37 @@ async function renderScenesTimeline(scenario, scenarioDir, onProgress) {
|
|
|
4311
4317
|
const outPath = join2(tmp, "timeline.mp4");
|
|
4312
4318
|
let audioInput = "";
|
|
4313
4319
|
let audioMap = "";
|
|
4320
|
+
const totalSec = totalMs / 1e3;
|
|
4314
4321
|
if (scenario.audio) {
|
|
4315
4322
|
const a = scenario.audio;
|
|
4316
|
-
|
|
4317
|
-
|
|
4323
|
+
let audioPath;
|
|
4324
|
+
if (/^https?:\/\//.test(a.file)) {
|
|
4325
|
+
const hash = createHash("sha256").update(a.file).digest("hex").slice(0, 16);
|
|
4326
|
+
audioPath = join2(tmpdir2(), `clipwise-audio-${hash}${a.file.match(/\.\w{2,4}$/)?.[0] ?? ".mp3"}`);
|
|
4327
|
+
if (!existsSync2(audioPath)) {
|
|
4328
|
+
execSync(`curl -sL -o "${audioPath}" "${a.file}"`, { stdio: ["ignore", "ignore", "pipe"] });
|
|
4329
|
+
}
|
|
4330
|
+
} else {
|
|
4331
|
+
audioPath = isAbsolute2(a.file) ? a.file : resolve2(scenarioDir, a.file);
|
|
4332
|
+
}
|
|
4318
4333
|
const af = [];
|
|
4319
4334
|
if (a.volume !== 1) af.push(`volume=${a.volume}`);
|
|
4320
4335
|
if (a.fadeIn > 0) af.push(`afade=t=in:d=${a.fadeIn / 1e3}`);
|
|
4321
4336
|
if (a.fadeOut > 0) af.push(`afade=t=out:st=${Math.max(0, totalSec - a.fadeOut / 1e3)}:d=${a.fadeOut / 1e3}`);
|
|
4322
|
-
audioInput = `-i "${audioPath}" `;
|
|
4323
|
-
audioMap = `-map ${segments.length}:a ${af.length ? `-af "${af.join(",")}" ` : ""}-c:a aac -b:a 192k
|
|
4337
|
+
audioInput = `-stream_loop -1 -i "${audioPath}" `;
|
|
4338
|
+
audioMap = `-map ${segments.length}:a ${af.length ? `-af "${af.join(",")}" ` : ""}-c:a aac -b:a 192k `;
|
|
4324
4339
|
}
|
|
4325
4340
|
execSync(
|
|
4326
|
-
`ffmpeg -y ${segments.map((s) => `-i "${s.path}"`).join(" ")} ${audioInput}-filter_complex "${filters};${concatInputs}concat=n=${segments.length}:v=1:a=0[v]" -map "[v]" ${audioMap}-c:v libx264 -crf 16 -preset slow -movflags +faststart "${outPath}"`,
|
|
4341
|
+
`ffmpeg -y ${segments.map((s) => `-i "${s.path}"`).join(" ")} ${audioInput}-filter_complex "${filters};${concatInputs}concat=n=${segments.length}:v=1:a=0[v]" -map "[v]" ${audioMap}-t ${totalSec.toFixed(3)} -c:v libx264 -crf 16 -preset slow -movflags +faststart "${outPath}"`,
|
|
4327
4342
|
{ stdio: ["ignore", "ignore", "pipe"] }
|
|
4328
4343
|
);
|
|
4329
4344
|
const buffer = await readFile4(outPath);
|
|
4330
4345
|
await rm2(tmp, { recursive: true, force: true }).catch(() => {
|
|
4331
4346
|
});
|
|
4347
|
+
for (const take of takes.values()) {
|
|
4348
|
+
await rm2(take.framesDir, { recursive: true, force: true }).catch(() => {
|
|
4349
|
+
});
|
|
4350
|
+
}
|
|
4332
4351
|
return buffer;
|
|
4333
4352
|
}
|
|
4334
4353
|
var init_runner = __esm({
|
|
@@ -4566,7 +4585,7 @@ import { homedir } from "os";
|
|
|
4566
4585
|
var program = new Command();
|
|
4567
4586
|
program.name("clipwise").description(
|
|
4568
4587
|
"Playwright-based cinematic screen recorder for product demos"
|
|
4569
|
-
).version("0.10.
|
|
4588
|
+
).version("0.10.1");
|
|
4570
4589
|
program.command("record").description("Record a demo from a YAML scenario file").argument("<scenario>", "Path to YAML scenario file").option("-o, --output <dir>", "Output directory (default: scenario outputDir or .clipwise/output)").option(
|
|
4571
4590
|
"-f, --format <format>",
|
|
4572
4591
|
"Output format (gif|mp4|png-sequence)"
|
package/dist/index.js
CHANGED
|
@@ -3524,6 +3524,7 @@ var StreamingSession = class extends EventEmitter {
|
|
|
3524
3524
|
// src/scenes/runner.ts
|
|
3525
3525
|
import { chromium as chromium2 } from "playwright";
|
|
3526
3526
|
import { createServer } from "http";
|
|
3527
|
+
import { createHash } from "crypto";
|
|
3527
3528
|
import { execSync } from "child_process";
|
|
3528
3529
|
import { readFile as readFile3, writeFile as writeFile2, mkdtemp, rm as rm2 } from "fs/promises";
|
|
3529
3530
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -3665,21 +3666,21 @@ async function recordFootageTake(scenario, scene, selectors) {
|
|
|
3665
3666
|
const recorder = new ClipwiseRecorder();
|
|
3666
3667
|
const session = await recorder.record(takeScenario);
|
|
3667
3668
|
const renderer = new CanvasRenderer(takeScenario.effects, segmentOutput(scenario), scene.steps);
|
|
3668
|
-
const
|
|
3669
|
-
|
|
3670
|
-
const
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3669
|
+
const framesDir = await mkdtemp(join2(tmpdir2(), `clipwise-footage-${scene.id}-`));
|
|
3670
|
+
let count = 0;
|
|
3671
|
+
for await (const f of renderer.composeStream(session.frames)) {
|
|
3672
|
+
const png = f.rawInfo ? await sharp10(f.buffer, {
|
|
3673
|
+
raw: { width: f.rawInfo.width, height: f.rawInfo.height, channels: f.rawInfo.channels }
|
|
3674
|
+
}).png().toBuffer() : f.buffer;
|
|
3675
|
+
await writeFile2(join2(framesDir, `${count}.png`), png);
|
|
3676
|
+
count++;
|
|
3677
|
+
}
|
|
3677
3678
|
const anchors = [];
|
|
3678
3679
|
for (let k = 0; k < scene.steps.length; k++) {
|
|
3679
3680
|
const idx = session.frames.findIndex((f) => (f.stepIndex ?? 0) >= k);
|
|
3680
3681
|
anchors.push(Math.max(0, idx) / scenario.output.fps);
|
|
3681
3682
|
}
|
|
3682
|
-
return {
|
|
3683
|
+
return { framesDir, count, anchors, boxes };
|
|
3683
3684
|
}
|
|
3684
3685
|
function fitCardW(cw, ch, maxW = 940, maxH = 540) {
|
|
3685
3686
|
return Math.round(Math.min(maxW, maxH * cw / ch));
|
|
@@ -3739,7 +3740,7 @@ function vignetteProps(scene, take, serverBase, scenario, brand, durMs) {
|
|
|
3739
3740
|
label: scene.label ?? "",
|
|
3740
3741
|
caption: scene.caption ?? "",
|
|
3741
3742
|
base: serverBase,
|
|
3742
|
-
count: take.
|
|
3743
|
+
count: take.count,
|
|
3743
3744
|
fps: scenario.output.fps,
|
|
3744
3745
|
start,
|
|
3745
3746
|
rate: scene.rate,
|
|
@@ -3797,9 +3798,14 @@ async function renderScenesTimeline(scenario, scenarioDir, onProgress) {
|
|
|
3797
3798
|
const m = req.url?.match(/^\/([\w-]+)\/(\d+)\.png$/);
|
|
3798
3799
|
const take = m ? takes.get(m[1]) : void 0;
|
|
3799
3800
|
if (take) {
|
|
3800
|
-
const idx = Math.min(take.
|
|
3801
|
-
|
|
3802
|
-
|
|
3801
|
+
const idx = Math.min(take.count - 1, parseInt(m[2], 10));
|
|
3802
|
+
readFile3(join2(take.framesDir, `${idx}.png`)).then((png) => {
|
|
3803
|
+
res.writeHead(200, { "content-type": "image/png", "cache-control": "max-age=3600" });
|
|
3804
|
+
res.end(png);
|
|
3805
|
+
}).catch(() => {
|
|
3806
|
+
res.writeHead(404);
|
|
3807
|
+
res.end();
|
|
3808
|
+
});
|
|
3803
3809
|
} else {
|
|
3804
3810
|
res.writeHead(404);
|
|
3805
3811
|
res.end();
|
|
@@ -3857,24 +3863,37 @@ async function renderScenesTimeline(scenario, scenarioDir, onProgress) {
|
|
|
3857
3863
|
const outPath = join2(tmp, "timeline.mp4");
|
|
3858
3864
|
let audioInput = "";
|
|
3859
3865
|
let audioMap = "";
|
|
3866
|
+
const totalSec = totalMs / 1e3;
|
|
3860
3867
|
if (scenario.audio) {
|
|
3861
3868
|
const a = scenario.audio;
|
|
3862
|
-
|
|
3863
|
-
|
|
3869
|
+
let audioPath;
|
|
3870
|
+
if (/^https?:\/\//.test(a.file)) {
|
|
3871
|
+
const hash = createHash("sha256").update(a.file).digest("hex").slice(0, 16);
|
|
3872
|
+
audioPath = join2(tmpdir2(), `clipwise-audio-${hash}${a.file.match(/\.\w{2,4}$/)?.[0] ?? ".mp3"}`);
|
|
3873
|
+
if (!existsSync2(audioPath)) {
|
|
3874
|
+
execSync(`curl -sL -o "${audioPath}" "${a.file}"`, { stdio: ["ignore", "ignore", "pipe"] });
|
|
3875
|
+
}
|
|
3876
|
+
} else {
|
|
3877
|
+
audioPath = isAbsolute(a.file) ? a.file : resolve(scenarioDir, a.file);
|
|
3878
|
+
}
|
|
3864
3879
|
const af = [];
|
|
3865
3880
|
if (a.volume !== 1) af.push(`volume=${a.volume}`);
|
|
3866
3881
|
if (a.fadeIn > 0) af.push(`afade=t=in:d=${a.fadeIn / 1e3}`);
|
|
3867
3882
|
if (a.fadeOut > 0) af.push(`afade=t=out:st=${Math.max(0, totalSec - a.fadeOut / 1e3)}:d=${a.fadeOut / 1e3}`);
|
|
3868
|
-
audioInput = `-i "${audioPath}" `;
|
|
3869
|
-
audioMap = `-map ${segments.length}:a ${af.length ? `-af "${af.join(",")}" ` : ""}-c:a aac -b:a 192k
|
|
3883
|
+
audioInput = `-stream_loop -1 -i "${audioPath}" `;
|
|
3884
|
+
audioMap = `-map ${segments.length}:a ${af.length ? `-af "${af.join(",")}" ` : ""}-c:a aac -b:a 192k `;
|
|
3870
3885
|
}
|
|
3871
3886
|
execSync(
|
|
3872
|
-
`ffmpeg -y ${segments.map((s) => `-i "${s.path}"`).join(" ")} ${audioInput}-filter_complex "${filters};${concatInputs}concat=n=${segments.length}:v=1:a=0[v]" -map "[v]" ${audioMap}-c:v libx264 -crf 16 -preset slow -movflags +faststart "${outPath}"`,
|
|
3887
|
+
`ffmpeg -y ${segments.map((s) => `-i "${s.path}"`).join(" ")} ${audioInput}-filter_complex "${filters};${concatInputs}concat=n=${segments.length}:v=1:a=0[v]" -map "[v]" ${audioMap}-t ${totalSec.toFixed(3)} -c:v libx264 -crf 16 -preset slow -movflags +faststart "${outPath}"`,
|
|
3873
3888
|
{ stdio: ["ignore", "ignore", "pipe"] }
|
|
3874
3889
|
);
|
|
3875
3890
|
const buffer = await readFile3(outPath);
|
|
3876
3891
|
await rm2(tmp, { recursive: true, force: true }).catch(() => {
|
|
3877
3892
|
});
|
|
3893
|
+
for (const take of takes.values()) {
|
|
3894
|
+
await rm2(take.framesDir, { recursive: true, force: true }).catch(() => {
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3878
3897
|
return buffer;
|
|
3879
3898
|
}
|
|
3880
3899
|
|
package/package.json
CHANGED
package/skills/clipwise.md
CHANGED
|
@@ -400,7 +400,10 @@ scenes:
|
|
|
400
400
|
7. **Sensitive data**: `prepare.mask: [".email", ".amount"]` blurs elements at record time
|
|
401
401
|
(follows scrolling — never ask the user to fake their data)
|
|
402
402
|
8. **Music**: `audio: { file: bgm.mp3, bpm: 122, fadeOut: 1500 }` muxes BGM into the final
|
|
403
|
-
video AND snaps every scene cut onto the beat grid (beat-synced cuts)
|
|
403
|
+
video AND snaps every scene cut onto the beat grid (beat-synced cuts).
|
|
404
|
+
`file:` also accepts a URL (downloaded+cached on the user's machine — use license-free
|
|
405
|
+
sources like Mixkit, e.g. `https://assets.mixkit.co/music/132/132.mp3`, ~120bpm).
|
|
406
|
+
Track shorter than the video loops automatically; video length is always authoritative
|
|
404
407
|
|
|
405
408
|
## Critical Rules
|
|
406
409
|
|