clipwise 0.9.1 → 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 +93 -30
- package/dist/index.d.ts +76 -26
- package/dist/index.js +92 -29
- package/package.json +1 -1
- package/skills/clipwise.md +13 -4
- package/templates/motion/vignette.html +22 -1
package/dist/cli/index.js
CHANGED
|
@@ -293,7 +293,9 @@ var init_types = __esm({
|
|
|
293
293
|
/** Fade-in duration in milliseconds. */
|
|
294
294
|
fadeIn: z.number().min(0).default(0),
|
|
295
295
|
/** Fade-out duration in milliseconds. */
|
|
296
|
-
fadeOut: z.number().min(0).default(0)
|
|
296
|
+
fadeOut: z.number().min(0).default(0),
|
|
297
|
+
/** 트랙 BPM — scenes 타임라인에서 지정 시 신 길이를 비트 격자에 스냅(비트 싱크 컷). */
|
|
298
|
+
bpm: z.number().min(40).max(220).optional()
|
|
297
299
|
});
|
|
298
300
|
AuthConfigSchema = z.object({
|
|
299
301
|
/** Path to a Playwright storageState JSON file (cookies + localStorage). */
|
|
@@ -324,6 +326,8 @@ var init_types = __esm({
|
|
|
324
326
|
PrepareConfigSchema = z.object({
|
|
325
327
|
/** 녹화에서 숨길 요소의 CSS 셀렉터 (쿠키 배너, dev 오버레이 등). */
|
|
326
328
|
hide: z.array(z.string().min(1)).default([]),
|
|
329
|
+
/** 블러 처리할 요소의 CSS 셀렉터 (이메일, 금액 등 민감 정보) — 스크롤·이동을 따라간다. */
|
|
330
|
+
mask: z.array(z.string().min(1)).default([]),
|
|
327
331
|
/** Date/Date.now를 이 시각으로 고정 (ISO 8601, 예: "2026-06-10T09:00:00Z"). */
|
|
328
332
|
freezeTime: z.string().optional(),
|
|
329
333
|
/** Math.random을 이 시드의 결정론적 PRNG로 대체. */
|
|
@@ -357,7 +361,7 @@ var init_types = __esm({
|
|
|
357
361
|
steps: z.array(StepSchema).min(1)
|
|
358
362
|
});
|
|
359
363
|
SceneFxSchema = z.object({
|
|
360
|
-
kind: z.enum(["circle", "arrow"]),
|
|
364
|
+
kind: z.enum(["circle", "arrow", "spotlight"]),
|
|
361
365
|
/** 대상 요소 셀렉터 — bounding box를 실측해 좌표로 사용. */
|
|
362
366
|
selector: z.string().optional(),
|
|
363
367
|
/** 명시 좌표 — circle: [x,y,w,h], arrow: [x1,y1,x2,y2]. */
|
|
@@ -387,8 +391,14 @@ var init_types = __esm({
|
|
|
387
391
|
/** 크롭 높이 상한 (px, 원본 기준) — 와이드 스트립 연출용. */
|
|
388
392
|
maxH: z.number().optional()
|
|
389
393
|
}).optional(),
|
|
390
|
-
/** 푸시인 카메라 (스케일 from→to).
|
|
391
|
-
|
|
394
|
+
/** 푸시인 카메라 (스케일 from→to).
|
|
395
|
+
* origin: 푸시 중심점 — 셀렉터(실측) 지정 시 그 요소를 향해 밀어 들어가
|
|
396
|
+
* 다음 신의 크롭과 이어지는 매치컷을 만든다. 기본은 화면 중앙 약간 위. */
|
|
397
|
+
push: z.object({
|
|
398
|
+
from: z.number().default(1),
|
|
399
|
+
to: z.number().default(1),
|
|
400
|
+
origin: z.string().optional()
|
|
401
|
+
}).optional(),
|
|
392
402
|
/** 푸티지 인용 시작점 — 초(number) 또는 step 경계 anchor. */
|
|
393
403
|
start: z.union([z.number(), z.object({ step: z.number().int().min(0), offset: z.number().default(0) })]).default(0),
|
|
394
404
|
/** 푸티지 재생 배속. */
|
|
@@ -571,6 +581,12 @@ function buildHideCss(selectors) {
|
|
|
571
581
|
visibility: hidden !important;
|
|
572
582
|
}`;
|
|
573
583
|
}
|
|
584
|
+
function buildMaskCss(selectors) {
|
|
585
|
+
return `${selectors.join(",\n")} {
|
|
586
|
+
filter: blur(10px) !important;
|
|
587
|
+
border-radius: 4px;
|
|
588
|
+
}`;
|
|
589
|
+
}
|
|
574
590
|
function buildCssInjectionScript(css) {
|
|
575
591
|
return `(() => {
|
|
576
592
|
const apply = () => {
|
|
@@ -648,6 +664,9 @@ async function applyPrepare(context, prepare) {
|
|
|
648
664
|
if (prepare.hide.length > 0) {
|
|
649
665
|
cssChunks.push(buildHideCss(prepare.hide));
|
|
650
666
|
}
|
|
667
|
+
if (prepare.mask.length > 0) {
|
|
668
|
+
cssChunks.push(buildMaskCss(prepare.mask));
|
|
669
|
+
}
|
|
651
670
|
if (prepare.inject?.css) {
|
|
652
671
|
const cssFiles = Array.isArray(prepare.inject.css) ? prepare.inject.css : [prepare.inject.css];
|
|
653
672
|
for (const file of cssFiles) {
|
|
@@ -3959,6 +3978,7 @@ __export(runner_exports, {
|
|
|
3959
3978
|
});
|
|
3960
3979
|
import { chromium as chromium2 } from "playwright";
|
|
3961
3980
|
import { createServer } from "http";
|
|
3981
|
+
import { createHash } from "crypto";
|
|
3962
3982
|
import { execSync } from "child_process";
|
|
3963
3983
|
import { readFile as readFile4, writeFile as writeFile2, mkdtemp, rm as rm2 } from "fs/promises";
|
|
3964
3984
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -4100,21 +4120,21 @@ async function recordFootageTake(scenario, scene, selectors) {
|
|
|
4100
4120
|
const recorder = new ClipwiseRecorder();
|
|
4101
4121
|
const session = await recorder.record(takeScenario);
|
|
4102
4122
|
const renderer = new CanvasRenderer(takeScenario.effects, segmentOutput(scenario), scene.steps);
|
|
4103
|
-
const
|
|
4104
|
-
|
|
4105
|
-
const
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
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
|
+
}
|
|
4112
4132
|
const anchors = [];
|
|
4113
4133
|
for (let k = 0; k < scene.steps.length; k++) {
|
|
4114
4134
|
const idx = session.frames.findIndex((f) => (f.stepIndex ?? 0) >= k);
|
|
4115
4135
|
anchors.push(Math.max(0, idx) / scenario.output.fps);
|
|
4116
4136
|
}
|
|
4117
|
-
return {
|
|
4137
|
+
return { framesDir, count, anchors, boxes };
|
|
4118
4138
|
}
|
|
4119
4139
|
function fitCardW(cw, ch, maxW = 940, maxH = 540) {
|
|
4120
4140
|
return Math.round(Math.min(maxW, maxH * cw / ch));
|
|
@@ -4144,7 +4164,7 @@ async function captureMotionSegment(templateUrl, props, durationMs, scenario) {
|
|
|
4144
4164
|
await browser.close();
|
|
4145
4165
|
return { buffer: await encodeMp4(frames, segmentOutput(scenario)), seconds: totalFrames / fps };
|
|
4146
4166
|
}
|
|
4147
|
-
function vignetteProps(scene, take, serverBase, scenario, brand) {
|
|
4167
|
+
function vignetteProps(scene, take, serverBase, scenario, brand, durMs) {
|
|
4148
4168
|
const W = scenario.viewport.width;
|
|
4149
4169
|
const H = scenario.viewport.height;
|
|
4150
4170
|
let crop = { x: 0, y: 0, w: W, h: H };
|
|
@@ -4168,13 +4188,13 @@ function vignetteProps(scene, take, serverBase, scenario, brand) {
|
|
|
4168
4188
|
const props = {
|
|
4169
4189
|
accent: brand.accent,
|
|
4170
4190
|
font: brand.font,
|
|
4171
|
-
dur:
|
|
4191
|
+
dur: durMs / 1e3,
|
|
4172
4192
|
layout: scene.layout,
|
|
4173
4193
|
num: scene.num ?? "",
|
|
4174
4194
|
label: scene.label ?? "",
|
|
4175
4195
|
caption: scene.caption ?? "",
|
|
4176
4196
|
base: serverBase,
|
|
4177
|
-
count: take.
|
|
4197
|
+
count: take.count,
|
|
4178
4198
|
fps: scenario.output.fps,
|
|
4179
4199
|
start,
|
|
4180
4200
|
rate: scene.rate,
|
|
@@ -4187,13 +4207,19 @@ function vignetteProps(scene, take, serverBase, scenario, brand) {
|
|
|
4187
4207
|
pushTo: scene.push?.to ?? 1
|
|
4188
4208
|
};
|
|
4189
4209
|
if (scene.code?.length) props.code = scene.code.join("||");
|
|
4210
|
+
if (scene.push?.origin) {
|
|
4211
|
+
const box = take.boxes.get(scene.push.origin);
|
|
4212
|
+
if (!box) throw new Error(`vignette push.origin selector "${scene.push.origin}" not found in footage "${scene.footage}"`);
|
|
4213
|
+
props.pushOx = Math.max(0, Math.min(100, (box.x + box.width / 2 - crop.x) / crop.w * 100)).toFixed(1);
|
|
4214
|
+
props.pushOy = Math.max(0, Math.min(100, (box.y + box.height / 2 - crop.y) / crop.h * 100)).toFixed(1);
|
|
4215
|
+
}
|
|
4190
4216
|
if (brand.annotations && scene.fx.length > 0) {
|
|
4191
4217
|
props.fx = scene.fx.map((fx) => {
|
|
4192
4218
|
let coords = fx.coords;
|
|
4193
4219
|
if (fx.selector) {
|
|
4194
4220
|
const box = take.boxes.get(fx.selector);
|
|
4195
4221
|
if (!box) throw new Error(`vignette fx selector "${fx.selector}" not found in footage "${scene.footage}"`);
|
|
4196
|
-
coords = fx.kind === "
|
|
4222
|
+
coords = fx.kind === "arrow" ? [box.x - 160, box.y + 120, box.x - 12, box.y + box.height / 2] : [box.x, box.y, box.width, box.height];
|
|
4197
4223
|
}
|
|
4198
4224
|
return `${fx.kind}@${coords.join(",")}@${fx.delay}`;
|
|
4199
4225
|
}).join(";");
|
|
@@ -4210,6 +4236,7 @@ async function renderScenesTimeline(scenario, scenarioDir, onProgress) {
|
|
|
4210
4236
|
if (scene.type !== "vignette") continue;
|
|
4211
4237
|
const set = selectorsByFootage.get(scene.footage) ?? /* @__PURE__ */ new Set();
|
|
4212
4238
|
if (scene.crop?.selector) set.add(scene.crop.selector);
|
|
4239
|
+
if (scene.push?.origin) set.add(scene.push.origin);
|
|
4213
4240
|
for (const fx of scene.fx) if (fx.selector) set.add(fx.selector);
|
|
4214
4241
|
selectorsByFootage.set(scene.footage, set);
|
|
4215
4242
|
}
|
|
@@ -4225,9 +4252,14 @@ async function renderScenesTimeline(scenario, scenarioDir, onProgress) {
|
|
|
4225
4252
|
const m = req.url?.match(/^\/([\w-]+)\/(\d+)\.png$/);
|
|
4226
4253
|
const take = m ? takes.get(m[1]) : void 0;
|
|
4227
4254
|
if (take) {
|
|
4228
|
-
const idx = Math.min(take.
|
|
4229
|
-
|
|
4230
|
-
|
|
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
|
+
});
|
|
4231
4263
|
} else {
|
|
4232
4264
|
res.writeHead(404);
|
|
4233
4265
|
res.end();
|
|
@@ -4235,27 +4267,32 @@ async function renderScenesTimeline(scenario, scenarioDir, onProgress) {
|
|
|
4235
4267
|
});
|
|
4236
4268
|
await new Promise((r) => server.listen(0, r));
|
|
4237
4269
|
const port = server.address().port;
|
|
4238
|
-
const
|
|
4270
|
+
const beatMs = scenario.audio?.bpm ? 6e4 / scenario.audio.bpm : 0;
|
|
4271
|
+
const durations = timeline.map(
|
|
4272
|
+
(sc) => beatMs ? Math.max(beatMs, Math.round(sc.duration / beatMs) * beatMs) : sc.duration
|
|
4273
|
+
);
|
|
4274
|
+
const totalMs = durations.reduce((s, d) => s + d, 0);
|
|
4239
4275
|
let elapsedMs = 0;
|
|
4240
4276
|
const segments = [];
|
|
4241
4277
|
const tmp = await mkdtemp(join2(tmpdir2(), "clipwise-scenes-"));
|
|
4242
4278
|
try {
|
|
4243
4279
|
for (let i = 0; i < timeline.length; i++) {
|
|
4244
4280
|
const scene = timeline[i];
|
|
4281
|
+
const dur = durations[i];
|
|
4245
4282
|
const label = scene.type === "motion" ? scene.template : `vignette(${scene.footage})`;
|
|
4246
4283
|
onProgress?.({ scene: i + 1, total: timeline.length, label });
|
|
4247
4284
|
const thread = brand.annotations ? {
|
|
4248
4285
|
threadFrom: (elapsedMs / totalMs).toFixed(4),
|
|
4249
|
-
threadTo: ((elapsedMs +
|
|
4286
|
+
threadTo: ((elapsedMs + dur) / totalMs).toFixed(4)
|
|
4250
4287
|
} : {};
|
|
4251
|
-
elapsedMs +=
|
|
4288
|
+
elapsedMs += dur;
|
|
4252
4289
|
let segment;
|
|
4253
4290
|
if (scene.type === "motion") {
|
|
4254
4291
|
const url = resolveMotionTemplate(scene.template, scenarioDir);
|
|
4255
4292
|
segment = await captureMotionSegment(
|
|
4256
4293
|
url,
|
|
4257
|
-
{ accent: brand.accent, font: brand.font, dur:
|
|
4258
|
-
|
|
4294
|
+
{ accent: brand.accent, font: brand.font, dur: dur / 1e3, ...scene.props, ...thread },
|
|
4295
|
+
dur,
|
|
4259
4296
|
scenario
|
|
4260
4297
|
);
|
|
4261
4298
|
} else {
|
|
@@ -4263,8 +4300,8 @@ async function renderScenesTimeline(scenario, scenarioDir, onProgress) {
|
|
|
4263
4300
|
const url = resolveMotionTemplate("vignette", scenarioDir);
|
|
4264
4301
|
segment = await captureMotionSegment(
|
|
4265
4302
|
url,
|
|
4266
|
-
{ ...vignetteProps(scene, take, `http://localhost:${port}/${scene.footage}`, scenario, brand), ...thread },
|
|
4267
|
-
|
|
4303
|
+
{ ...vignetteProps(scene, take, `http://localhost:${port}/${scene.footage}`, scenario, brand, dur), ...thread },
|
|
4304
|
+
dur,
|
|
4268
4305
|
scenario
|
|
4269
4306
|
);
|
|
4270
4307
|
}
|
|
@@ -4278,13 +4315,39 @@ async function renderScenesTimeline(scenario, scenarioDir, onProgress) {
|
|
|
4278
4315
|
const filters = segments.map((_, i) => `[${i}:v]format=yuv420p[v${i}]`).join(";");
|
|
4279
4316
|
const concatInputs = segments.map((_, i) => `[v${i}]`).join("");
|
|
4280
4317
|
const outPath = join2(tmp, "timeline.mp4");
|
|
4318
|
+
let audioInput = "";
|
|
4319
|
+
let audioMap = "";
|
|
4320
|
+
const totalSec = totalMs / 1e3;
|
|
4321
|
+
if (scenario.audio) {
|
|
4322
|
+
const a = scenario.audio;
|
|
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
|
+
}
|
|
4333
|
+
const af = [];
|
|
4334
|
+
if (a.volume !== 1) af.push(`volume=${a.volume}`);
|
|
4335
|
+
if (a.fadeIn > 0) af.push(`afade=t=in:d=${a.fadeIn / 1e3}`);
|
|
4336
|
+
if (a.fadeOut > 0) af.push(`afade=t=out:st=${Math.max(0, totalSec - a.fadeOut / 1e3)}:d=${a.fadeOut / 1e3}`);
|
|
4337
|
+
audioInput = `-stream_loop -1 -i "${audioPath}" `;
|
|
4338
|
+
audioMap = `-map ${segments.length}:a ${af.length ? `-af "${af.join(",")}" ` : ""}-c:a aac -b:a 192k `;
|
|
4339
|
+
}
|
|
4281
4340
|
execSync(
|
|
4282
|
-
`ffmpeg -y ${segments.map((s) => `-i "${s.path}"`).join(" ")} -filter_complex "${filters};${concatInputs}concat=n=${segments.length}:v=1:a=0[v]" -map "[v]" -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}"`,
|
|
4283
4342
|
{ stdio: ["ignore", "ignore", "pipe"] }
|
|
4284
4343
|
);
|
|
4285
4344
|
const buffer = await readFile4(outPath);
|
|
4286
4345
|
await rm2(tmp, { recursive: true, force: true }).catch(() => {
|
|
4287
4346
|
});
|
|
4347
|
+
for (const take of takes.values()) {
|
|
4348
|
+
await rm2(take.framesDir, { recursive: true, force: true }).catch(() => {
|
|
4349
|
+
});
|
|
4350
|
+
}
|
|
4288
4351
|
return buffer;
|
|
4289
4352
|
}
|
|
4290
4353
|
var init_runner = __esm({
|
|
@@ -4522,7 +4585,7 @@ import { homedir } from "os";
|
|
|
4522
4585
|
var program = new Command();
|
|
4523
4586
|
program.name("clipwise").description(
|
|
4524
4587
|
"Playwright-based cinematic screen recorder for product demos"
|
|
4525
|
-
).version("0.
|
|
4588
|
+
).version("0.10.1");
|
|
4526
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(
|
|
4527
4590
|
"-f, --format <format>",
|
|
4528
4591
|
"Output format (gif|mp4|png-sequence)"
|
package/dist/index.d.ts
CHANGED
|
@@ -1934,16 +1934,20 @@ declare const AudioConfigSchema: z.ZodObject<{
|
|
|
1934
1934
|
fadeIn: z.ZodDefault<z.ZodNumber>;
|
|
1935
1935
|
/** Fade-out duration in milliseconds. */
|
|
1936
1936
|
fadeOut: z.ZodDefault<z.ZodNumber>;
|
|
1937
|
+
/** 트랙 BPM — scenes 타임라인에서 지정 시 신 길이를 비트 격자에 스냅(비트 싱크 컷). */
|
|
1938
|
+
bpm: z.ZodOptional<z.ZodNumber>;
|
|
1937
1939
|
}, "strip", z.ZodTypeAny, {
|
|
1938
1940
|
file: string;
|
|
1939
1941
|
volume: number;
|
|
1940
1942
|
fadeIn: number;
|
|
1941
1943
|
fadeOut: number;
|
|
1944
|
+
bpm?: number | undefined;
|
|
1942
1945
|
}, {
|
|
1943
1946
|
file: string;
|
|
1944
1947
|
volume?: number | undefined;
|
|
1945
1948
|
fadeIn?: number | undefined;
|
|
1946
1949
|
fadeOut?: number | undefined;
|
|
1950
|
+
bpm?: number | undefined;
|
|
1947
1951
|
}>;
|
|
1948
1952
|
type AudioConfig = z.infer<typeof AudioConfigSchema>;
|
|
1949
1953
|
/**
|
|
@@ -2045,6 +2049,8 @@ type MockRoute = z.infer<typeof MockRouteSchema>;
|
|
|
2045
2049
|
declare const PrepareConfigSchema: z.ZodObject<{
|
|
2046
2050
|
/** 녹화에서 숨길 요소의 CSS 셀렉터 (쿠키 배너, dev 오버레이 등). */
|
|
2047
2051
|
hide: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
2052
|
+
/** 블러 처리할 요소의 CSS 셀렉터 (이메일, 금액 등 민감 정보) — 스크롤·이동을 따라간다. */
|
|
2053
|
+
mask: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
2048
2054
|
/** Date/Date.now를 이 시각으로 고정 (ISO 8601, 예: "2026-06-10T09:00:00Z"). */
|
|
2049
2055
|
freezeTime: z.ZodOptional<z.ZodString>;
|
|
2050
2056
|
/** Math.random을 이 시드의 결정론적 PRNG로 대체. */
|
|
@@ -2096,6 +2102,7 @@ declare const PrepareConfigSchema: z.ZodObject<{
|
|
|
2096
2102
|
}>>;
|
|
2097
2103
|
}, "strip", z.ZodTypeAny, {
|
|
2098
2104
|
hide: string[];
|
|
2105
|
+
mask: string[];
|
|
2099
2106
|
mock: {
|
|
2100
2107
|
status: number;
|
|
2101
2108
|
url: string;
|
|
@@ -2115,6 +2122,7 @@ declare const PrepareConfigSchema: z.ZodObject<{
|
|
|
2115
2122
|
} | undefined;
|
|
2116
2123
|
}, {
|
|
2117
2124
|
hide?: string[] | undefined;
|
|
2125
|
+
mask?: string[] | undefined;
|
|
2118
2126
|
freezeTime?: string | undefined;
|
|
2119
2127
|
seedRandom?: number | undefined;
|
|
2120
2128
|
storage?: {
|
|
@@ -3374,9 +3382,10 @@ declare const ScreenSceneSchema: z.ZodObject<{
|
|
|
3374
3382
|
} | undefined;
|
|
3375
3383
|
}[];
|
|
3376
3384
|
}>;
|
|
3377
|
-
/**
|
|
3385
|
+
/** 푸티지 주석 — circle/arrow(선 드로잉), spotlight(주변 디밍).
|
|
3386
|
+
* 좌표는 셀렉터 실측(권장) 또는 푸티지 원본 px. */
|
|
3378
3387
|
declare const SceneFxSchema: z.ZodObject<{
|
|
3379
|
-
kind: z.ZodEnum<["circle", "arrow"]>;
|
|
3388
|
+
kind: z.ZodEnum<["circle", "arrow", "spotlight"]>;
|
|
3380
3389
|
/** 대상 요소 셀렉터 — bounding box를 실측해 좌표로 사용. */
|
|
3381
3390
|
selector: z.ZodOptional<z.ZodString>;
|
|
3382
3391
|
/** 명시 좌표 — circle: [x,y,w,h], arrow: [x1,y1,x2,y2]. */
|
|
@@ -3385,11 +3394,11 @@ declare const SceneFxSchema: z.ZodObject<{
|
|
|
3385
3394
|
delay: z.ZodDefault<z.ZodNumber>;
|
|
3386
3395
|
}, "strip", z.ZodTypeAny, {
|
|
3387
3396
|
delay: number;
|
|
3388
|
-
kind: "circle" | "arrow";
|
|
3397
|
+
kind: "circle" | "arrow" | "spotlight";
|
|
3389
3398
|
selector?: string | undefined;
|
|
3390
3399
|
coords?: number[] | undefined;
|
|
3391
3400
|
}, {
|
|
3392
|
-
kind: "circle" | "arrow";
|
|
3401
|
+
kind: "circle" | "arrow" | "spotlight";
|
|
3393
3402
|
selector?: string | undefined;
|
|
3394
3403
|
delay?: number | undefined;
|
|
3395
3404
|
coords?: number[] | undefined;
|
|
@@ -3433,16 +3442,21 @@ declare const VignetteSceneSchema: z.ZodObject<{
|
|
|
3433
3442
|
h?: number | undefined;
|
|
3434
3443
|
maxH?: number | undefined;
|
|
3435
3444
|
}>>;
|
|
3436
|
-
/** 푸시인 카메라 (스케일 from→to).
|
|
3445
|
+
/** 푸시인 카메라 (스케일 from→to).
|
|
3446
|
+
* origin: 푸시 중심점 — 셀렉터(실측) 지정 시 그 요소를 향해 밀어 들어가
|
|
3447
|
+
* 다음 신의 크롭과 이어지는 매치컷을 만든다. 기본은 화면 중앙 약간 위. */
|
|
3437
3448
|
push: z.ZodOptional<z.ZodObject<{
|
|
3438
3449
|
from: z.ZodDefault<z.ZodNumber>;
|
|
3439
3450
|
to: z.ZodDefault<z.ZodNumber>;
|
|
3451
|
+
origin: z.ZodOptional<z.ZodString>;
|
|
3440
3452
|
}, "strip", z.ZodTypeAny, {
|
|
3441
3453
|
from: number;
|
|
3442
3454
|
to: number;
|
|
3455
|
+
origin?: string | undefined;
|
|
3443
3456
|
}, {
|
|
3444
3457
|
from?: number | undefined;
|
|
3445
3458
|
to?: number | undefined;
|
|
3459
|
+
origin?: string | undefined;
|
|
3446
3460
|
}>>;
|
|
3447
3461
|
/** 푸티지 인용 시작점 — 초(number) 또는 step 경계 anchor. */
|
|
3448
3462
|
start: z.ZodDefault<z.ZodUnion<[z.ZodNumber, z.ZodObject<{
|
|
@@ -3458,7 +3472,7 @@ declare const VignetteSceneSchema: z.ZodObject<{
|
|
|
3458
3472
|
/** 푸티지 재생 배속. */
|
|
3459
3473
|
rate: z.ZodDefault<z.ZodNumber>;
|
|
3460
3474
|
fx: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
3461
|
-
kind: z.ZodEnum<["circle", "arrow"]>;
|
|
3475
|
+
kind: z.ZodEnum<["circle", "arrow", "spotlight"]>;
|
|
3462
3476
|
/** 대상 요소 셀렉터 — bounding box를 실측해 좌표로 사용. */
|
|
3463
3477
|
selector: z.ZodOptional<z.ZodString>;
|
|
3464
3478
|
/** 명시 좌표 — circle: [x,y,w,h], arrow: [x1,y1,x2,y2]. */
|
|
@@ -3467,11 +3481,11 @@ declare const VignetteSceneSchema: z.ZodObject<{
|
|
|
3467
3481
|
delay: z.ZodDefault<z.ZodNumber>;
|
|
3468
3482
|
}, "strip", z.ZodTypeAny, {
|
|
3469
3483
|
delay: number;
|
|
3470
|
-
kind: "circle" | "arrow";
|
|
3484
|
+
kind: "circle" | "arrow" | "spotlight";
|
|
3471
3485
|
selector?: string | undefined;
|
|
3472
3486
|
coords?: number[] | undefined;
|
|
3473
3487
|
}, {
|
|
3474
|
-
kind: "circle" | "arrow";
|
|
3488
|
+
kind: "circle" | "arrow" | "spotlight";
|
|
3475
3489
|
selector?: string | undefined;
|
|
3476
3490
|
delay?: number | undefined;
|
|
3477
3491
|
coords?: number[] | undefined;
|
|
@@ -3488,13 +3502,14 @@ declare const VignetteSceneSchema: z.ZodObject<{
|
|
|
3488
3502
|
rate: number;
|
|
3489
3503
|
fx: {
|
|
3490
3504
|
delay: number;
|
|
3491
|
-
kind: "circle" | "arrow";
|
|
3505
|
+
kind: "circle" | "arrow" | "spotlight";
|
|
3492
3506
|
selector?: string | undefined;
|
|
3493
3507
|
coords?: number[] | undefined;
|
|
3494
3508
|
}[];
|
|
3495
3509
|
push?: {
|
|
3496
3510
|
from: number;
|
|
3497
3511
|
to: number;
|
|
3512
|
+
origin?: string | undefined;
|
|
3498
3513
|
} | undefined;
|
|
3499
3514
|
code?: string[] | undefined;
|
|
3500
3515
|
crop?: {
|
|
@@ -3516,6 +3531,7 @@ declare const VignetteSceneSchema: z.ZodObject<{
|
|
|
3516
3531
|
push?: {
|
|
3517
3532
|
from?: number | undefined;
|
|
3518
3533
|
to?: number | undefined;
|
|
3534
|
+
origin?: string | undefined;
|
|
3519
3535
|
} | undefined;
|
|
3520
3536
|
code?: string[] | undefined;
|
|
3521
3537
|
crop?: {
|
|
@@ -3537,7 +3553,7 @@ declare const VignetteSceneSchema: z.ZodObject<{
|
|
|
3537
3553
|
} | undefined;
|
|
3538
3554
|
rate?: number | undefined;
|
|
3539
3555
|
fx?: {
|
|
3540
|
-
kind: "circle" | "arrow";
|
|
3556
|
+
kind: "circle" | "arrow" | "spotlight";
|
|
3541
3557
|
selector?: string | undefined;
|
|
3542
3558
|
delay?: number | undefined;
|
|
3543
3559
|
coords?: number[] | undefined;
|
|
@@ -4817,16 +4833,21 @@ declare const SceneSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
|
4817
4833
|
h?: number | undefined;
|
|
4818
4834
|
maxH?: number | undefined;
|
|
4819
4835
|
}>>;
|
|
4820
|
-
/** 푸시인 카메라 (스케일 from→to).
|
|
4836
|
+
/** 푸시인 카메라 (스케일 from→to).
|
|
4837
|
+
* origin: 푸시 중심점 — 셀렉터(실측) 지정 시 그 요소를 향해 밀어 들어가
|
|
4838
|
+
* 다음 신의 크롭과 이어지는 매치컷을 만든다. 기본은 화면 중앙 약간 위. */
|
|
4821
4839
|
push: z.ZodOptional<z.ZodObject<{
|
|
4822
4840
|
from: z.ZodDefault<z.ZodNumber>;
|
|
4823
4841
|
to: z.ZodDefault<z.ZodNumber>;
|
|
4842
|
+
origin: z.ZodOptional<z.ZodString>;
|
|
4824
4843
|
}, "strip", z.ZodTypeAny, {
|
|
4825
4844
|
from: number;
|
|
4826
4845
|
to: number;
|
|
4846
|
+
origin?: string | undefined;
|
|
4827
4847
|
}, {
|
|
4828
4848
|
from?: number | undefined;
|
|
4829
4849
|
to?: number | undefined;
|
|
4850
|
+
origin?: string | undefined;
|
|
4830
4851
|
}>>;
|
|
4831
4852
|
/** 푸티지 인용 시작점 — 초(number) 또는 step 경계 anchor. */
|
|
4832
4853
|
start: z.ZodDefault<z.ZodUnion<[z.ZodNumber, z.ZodObject<{
|
|
@@ -4842,7 +4863,7 @@ declare const SceneSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
|
4842
4863
|
/** 푸티지 재생 배속. */
|
|
4843
4864
|
rate: z.ZodDefault<z.ZodNumber>;
|
|
4844
4865
|
fx: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
4845
|
-
kind: z.ZodEnum<["circle", "arrow"]>;
|
|
4866
|
+
kind: z.ZodEnum<["circle", "arrow", "spotlight"]>;
|
|
4846
4867
|
/** 대상 요소 셀렉터 — bounding box를 실측해 좌표로 사용. */
|
|
4847
4868
|
selector: z.ZodOptional<z.ZodString>;
|
|
4848
4869
|
/** 명시 좌표 — circle: [x,y,w,h], arrow: [x1,y1,x2,y2]. */
|
|
@@ -4851,11 +4872,11 @@ declare const SceneSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
|
4851
4872
|
delay: z.ZodDefault<z.ZodNumber>;
|
|
4852
4873
|
}, "strip", z.ZodTypeAny, {
|
|
4853
4874
|
delay: number;
|
|
4854
|
-
kind: "circle" | "arrow";
|
|
4875
|
+
kind: "circle" | "arrow" | "spotlight";
|
|
4855
4876
|
selector?: string | undefined;
|
|
4856
4877
|
coords?: number[] | undefined;
|
|
4857
4878
|
}, {
|
|
4858
|
-
kind: "circle" | "arrow";
|
|
4879
|
+
kind: "circle" | "arrow" | "spotlight";
|
|
4859
4880
|
selector?: string | undefined;
|
|
4860
4881
|
delay?: number | undefined;
|
|
4861
4882
|
coords?: number[] | undefined;
|
|
@@ -4872,13 +4893,14 @@ declare const SceneSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
|
4872
4893
|
rate: number;
|
|
4873
4894
|
fx: {
|
|
4874
4895
|
delay: number;
|
|
4875
|
-
kind: "circle" | "arrow";
|
|
4896
|
+
kind: "circle" | "arrow" | "spotlight";
|
|
4876
4897
|
selector?: string | undefined;
|
|
4877
4898
|
coords?: number[] | undefined;
|
|
4878
4899
|
}[];
|
|
4879
4900
|
push?: {
|
|
4880
4901
|
from: number;
|
|
4881
4902
|
to: number;
|
|
4903
|
+
origin?: string | undefined;
|
|
4882
4904
|
} | undefined;
|
|
4883
4905
|
code?: string[] | undefined;
|
|
4884
4906
|
crop?: {
|
|
@@ -4900,6 +4922,7 @@ declare const SceneSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
|
4900
4922
|
push?: {
|
|
4901
4923
|
from?: number | undefined;
|
|
4902
4924
|
to?: number | undefined;
|
|
4925
|
+
origin?: string | undefined;
|
|
4903
4926
|
} | undefined;
|
|
4904
4927
|
code?: string[] | undefined;
|
|
4905
4928
|
crop?: {
|
|
@@ -4921,7 +4944,7 @@ declare const SceneSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
|
4921
4944
|
} | undefined;
|
|
4922
4945
|
rate?: number | undefined;
|
|
4923
4946
|
fx?: {
|
|
4924
|
-
kind: "circle" | "arrow";
|
|
4947
|
+
kind: "circle" | "arrow" | "spotlight";
|
|
4925
4948
|
selector?: string | undefined;
|
|
4926
4949
|
delay?: number | undefined;
|
|
4927
4950
|
coords?: number[] | undefined;
|
|
@@ -5006,6 +5029,8 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
5006
5029
|
prepare: z.ZodOptional<z.ZodObject<{
|
|
5007
5030
|
/** 녹화에서 숨길 요소의 CSS 셀렉터 (쿠키 배너, dev 오버레이 등). */
|
|
5008
5031
|
hide: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
5032
|
+
/** 블러 처리할 요소의 CSS 셀렉터 (이메일, 금액 등 민감 정보) — 스크롤·이동을 따라간다. */
|
|
5033
|
+
mask: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
5009
5034
|
/** Date/Date.now를 이 시각으로 고정 (ISO 8601, 예: "2026-06-10T09:00:00Z"). */
|
|
5010
5035
|
freezeTime: z.ZodOptional<z.ZodString>;
|
|
5011
5036
|
/** Math.random을 이 시드의 결정론적 PRNG로 대체. */
|
|
@@ -5057,6 +5082,7 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
5057
5082
|
}>>;
|
|
5058
5083
|
}, "strip", z.ZodTypeAny, {
|
|
5059
5084
|
hide: string[];
|
|
5085
|
+
mask: string[];
|
|
5060
5086
|
mock: {
|
|
5061
5087
|
status: number;
|
|
5062
5088
|
url: string;
|
|
@@ -5076,6 +5102,7 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
5076
5102
|
} | undefined;
|
|
5077
5103
|
}, {
|
|
5078
5104
|
hide?: string[] | undefined;
|
|
5105
|
+
mask?: string[] | undefined;
|
|
5079
5106
|
freezeTime?: string | undefined;
|
|
5080
5107
|
seedRandom?: number | undefined;
|
|
5081
5108
|
storage?: {
|
|
@@ -5525,16 +5552,20 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
5525
5552
|
fadeIn: z.ZodDefault<z.ZodNumber>;
|
|
5526
5553
|
/** Fade-out duration in milliseconds. */
|
|
5527
5554
|
fadeOut: z.ZodDefault<z.ZodNumber>;
|
|
5555
|
+
/** 트랙 BPM — scenes 타임라인에서 지정 시 신 길이를 비트 격자에 스냅(비트 싱크 컷). */
|
|
5556
|
+
bpm: z.ZodOptional<z.ZodNumber>;
|
|
5528
5557
|
}, "strip", z.ZodTypeAny, {
|
|
5529
5558
|
file: string;
|
|
5530
5559
|
volume: number;
|
|
5531
5560
|
fadeIn: number;
|
|
5532
5561
|
fadeOut: number;
|
|
5562
|
+
bpm?: number | undefined;
|
|
5533
5563
|
}, {
|
|
5534
5564
|
file: string;
|
|
5535
5565
|
volume?: number | undefined;
|
|
5536
5566
|
fadeIn?: number | undefined;
|
|
5537
5567
|
fadeOut?: number | undefined;
|
|
5568
|
+
bpm?: number | undefined;
|
|
5538
5569
|
}>>;
|
|
5539
5570
|
/** steps 기반(클래식) 시나리오. scenes가 있으면 생략 가능. */
|
|
5540
5571
|
steps: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
@@ -7712,16 +7743,21 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
7712
7743
|
h?: number | undefined;
|
|
7713
7744
|
maxH?: number | undefined;
|
|
7714
7745
|
}>>;
|
|
7715
|
-
/** 푸시인 카메라 (스케일 from→to).
|
|
7746
|
+
/** 푸시인 카메라 (스케일 from→to).
|
|
7747
|
+
* origin: 푸시 중심점 — 셀렉터(실측) 지정 시 그 요소를 향해 밀어 들어가
|
|
7748
|
+
* 다음 신의 크롭과 이어지는 매치컷을 만든다. 기본은 화면 중앙 약간 위. */
|
|
7716
7749
|
push: z.ZodOptional<z.ZodObject<{
|
|
7717
7750
|
from: z.ZodDefault<z.ZodNumber>;
|
|
7718
7751
|
to: z.ZodDefault<z.ZodNumber>;
|
|
7752
|
+
origin: z.ZodOptional<z.ZodString>;
|
|
7719
7753
|
}, "strip", z.ZodTypeAny, {
|
|
7720
7754
|
from: number;
|
|
7721
7755
|
to: number;
|
|
7756
|
+
origin?: string | undefined;
|
|
7722
7757
|
}, {
|
|
7723
7758
|
from?: number | undefined;
|
|
7724
7759
|
to?: number | undefined;
|
|
7760
|
+
origin?: string | undefined;
|
|
7725
7761
|
}>>;
|
|
7726
7762
|
/** 푸티지 인용 시작점 — 초(number) 또는 step 경계 anchor. */
|
|
7727
7763
|
start: z.ZodDefault<z.ZodUnion<[z.ZodNumber, z.ZodObject<{
|
|
@@ -7737,7 +7773,7 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
7737
7773
|
/** 푸티지 재생 배속. */
|
|
7738
7774
|
rate: z.ZodDefault<z.ZodNumber>;
|
|
7739
7775
|
fx: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
7740
|
-
kind: z.ZodEnum<["circle", "arrow"]>;
|
|
7776
|
+
kind: z.ZodEnum<["circle", "arrow", "spotlight"]>;
|
|
7741
7777
|
/** 대상 요소 셀렉터 — bounding box를 실측해 좌표로 사용. */
|
|
7742
7778
|
selector: z.ZodOptional<z.ZodString>;
|
|
7743
7779
|
/** 명시 좌표 — circle: [x,y,w,h], arrow: [x1,y1,x2,y2]. */
|
|
@@ -7746,11 +7782,11 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
7746
7782
|
delay: z.ZodDefault<z.ZodNumber>;
|
|
7747
7783
|
}, "strip", z.ZodTypeAny, {
|
|
7748
7784
|
delay: number;
|
|
7749
|
-
kind: "circle" | "arrow";
|
|
7785
|
+
kind: "circle" | "arrow" | "spotlight";
|
|
7750
7786
|
selector?: string | undefined;
|
|
7751
7787
|
coords?: number[] | undefined;
|
|
7752
7788
|
}, {
|
|
7753
|
-
kind: "circle" | "arrow";
|
|
7789
|
+
kind: "circle" | "arrow" | "spotlight";
|
|
7754
7790
|
selector?: string | undefined;
|
|
7755
7791
|
delay?: number | undefined;
|
|
7756
7792
|
coords?: number[] | undefined;
|
|
@@ -7767,13 +7803,14 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
7767
7803
|
rate: number;
|
|
7768
7804
|
fx: {
|
|
7769
7805
|
delay: number;
|
|
7770
|
-
kind: "circle" | "arrow";
|
|
7806
|
+
kind: "circle" | "arrow" | "spotlight";
|
|
7771
7807
|
selector?: string | undefined;
|
|
7772
7808
|
coords?: number[] | undefined;
|
|
7773
7809
|
}[];
|
|
7774
7810
|
push?: {
|
|
7775
7811
|
from: number;
|
|
7776
7812
|
to: number;
|
|
7813
|
+
origin?: string | undefined;
|
|
7777
7814
|
} | undefined;
|
|
7778
7815
|
code?: string[] | undefined;
|
|
7779
7816
|
crop?: {
|
|
@@ -7795,6 +7832,7 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
7795
7832
|
push?: {
|
|
7796
7833
|
from?: number | undefined;
|
|
7797
7834
|
to?: number | undefined;
|
|
7835
|
+
origin?: string | undefined;
|
|
7798
7836
|
} | undefined;
|
|
7799
7837
|
code?: string[] | undefined;
|
|
7800
7838
|
crop?: {
|
|
@@ -7816,7 +7854,7 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
7816
7854
|
} | undefined;
|
|
7817
7855
|
rate?: number | undefined;
|
|
7818
7856
|
fx?: {
|
|
7819
|
-
kind: "circle" | "arrow";
|
|
7857
|
+
kind: "circle" | "arrow" | "spotlight";
|
|
7820
7858
|
selector?: string | undefined;
|
|
7821
7859
|
delay?: number | undefined;
|
|
7822
7860
|
coords?: number[] | undefined;
|
|
@@ -8084,6 +8122,7 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
8084
8122
|
} | undefined;
|
|
8085
8123
|
prepare?: {
|
|
8086
8124
|
hide: string[];
|
|
8125
|
+
mask: string[];
|
|
8087
8126
|
mock: {
|
|
8088
8127
|
status: number;
|
|
8089
8128
|
url: string;
|
|
@@ -8107,6 +8146,7 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
8107
8146
|
volume: number;
|
|
8108
8147
|
fadeIn: number;
|
|
8109
8148
|
fadeOut: number;
|
|
8149
|
+
bpm?: number | undefined;
|
|
8110
8150
|
} | undefined;
|
|
8111
8151
|
scenes?: ({
|
|
8112
8152
|
type: "motion";
|
|
@@ -8282,13 +8322,14 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
8282
8322
|
rate: number;
|
|
8283
8323
|
fx: {
|
|
8284
8324
|
delay: number;
|
|
8285
|
-
kind: "circle" | "arrow";
|
|
8325
|
+
kind: "circle" | "arrow" | "spotlight";
|
|
8286
8326
|
selector?: string | undefined;
|
|
8287
8327
|
coords?: number[] | undefined;
|
|
8288
8328
|
}[];
|
|
8289
8329
|
push?: {
|
|
8290
8330
|
from: number;
|
|
8291
8331
|
to: number;
|
|
8332
|
+
origin?: string | undefined;
|
|
8292
8333
|
} | undefined;
|
|
8293
8334
|
code?: string[] | undefined;
|
|
8294
8335
|
crop?: {
|
|
@@ -8555,6 +8596,7 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
8555
8596
|
} | undefined;
|
|
8556
8597
|
prepare?: {
|
|
8557
8598
|
hide?: string[] | undefined;
|
|
8599
|
+
mask?: string[] | undefined;
|
|
8558
8600
|
freezeTime?: string | undefined;
|
|
8559
8601
|
seedRandom?: number | undefined;
|
|
8560
8602
|
storage?: {
|
|
@@ -8589,6 +8631,7 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
8589
8631
|
volume?: number | undefined;
|
|
8590
8632
|
fadeIn?: number | undefined;
|
|
8591
8633
|
fadeOut?: number | undefined;
|
|
8634
|
+
bpm?: number | undefined;
|
|
8592
8635
|
} | undefined;
|
|
8593
8636
|
scenes?: ({
|
|
8594
8637
|
type: "motion";
|
|
@@ -8759,6 +8802,7 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
8759
8802
|
push?: {
|
|
8760
8803
|
from?: number | undefined;
|
|
8761
8804
|
to?: number | undefined;
|
|
8805
|
+
origin?: string | undefined;
|
|
8762
8806
|
} | undefined;
|
|
8763
8807
|
code?: string[] | undefined;
|
|
8764
8808
|
crop?: {
|
|
@@ -8780,7 +8824,7 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
8780
8824
|
} | undefined;
|
|
8781
8825
|
rate?: number | undefined;
|
|
8782
8826
|
fx?: {
|
|
8783
|
-
kind: "circle" | "arrow";
|
|
8827
|
+
kind: "circle" | "arrow" | "spotlight";
|
|
8784
8828
|
selector?: string | undefined;
|
|
8785
8829
|
delay?: number | undefined;
|
|
8786
8830
|
coords?: number[] | undefined;
|
|
@@ -9048,6 +9092,7 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
9048
9092
|
} | undefined;
|
|
9049
9093
|
prepare?: {
|
|
9050
9094
|
hide: string[];
|
|
9095
|
+
mask: string[];
|
|
9051
9096
|
mock: {
|
|
9052
9097
|
status: number;
|
|
9053
9098
|
url: string;
|
|
@@ -9071,6 +9116,7 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
9071
9116
|
volume: number;
|
|
9072
9117
|
fadeIn: number;
|
|
9073
9118
|
fadeOut: number;
|
|
9119
|
+
bpm?: number | undefined;
|
|
9074
9120
|
} | undefined;
|
|
9075
9121
|
scenes?: ({
|
|
9076
9122
|
type: "motion";
|
|
@@ -9246,13 +9292,14 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
9246
9292
|
rate: number;
|
|
9247
9293
|
fx: {
|
|
9248
9294
|
delay: number;
|
|
9249
|
-
kind: "circle" | "arrow";
|
|
9295
|
+
kind: "circle" | "arrow" | "spotlight";
|
|
9250
9296
|
selector?: string | undefined;
|
|
9251
9297
|
coords?: number[] | undefined;
|
|
9252
9298
|
}[];
|
|
9253
9299
|
push?: {
|
|
9254
9300
|
from: number;
|
|
9255
9301
|
to: number;
|
|
9302
|
+
origin?: string | undefined;
|
|
9256
9303
|
} | undefined;
|
|
9257
9304
|
code?: string[] | undefined;
|
|
9258
9305
|
crop?: {
|
|
@@ -9519,6 +9566,7 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
9519
9566
|
} | undefined;
|
|
9520
9567
|
prepare?: {
|
|
9521
9568
|
hide?: string[] | undefined;
|
|
9569
|
+
mask?: string[] | undefined;
|
|
9522
9570
|
freezeTime?: string | undefined;
|
|
9523
9571
|
seedRandom?: number | undefined;
|
|
9524
9572
|
storage?: {
|
|
@@ -9553,6 +9601,7 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
9553
9601
|
volume?: number | undefined;
|
|
9554
9602
|
fadeIn?: number | undefined;
|
|
9555
9603
|
fadeOut?: number | undefined;
|
|
9604
|
+
bpm?: number | undefined;
|
|
9556
9605
|
} | undefined;
|
|
9557
9606
|
scenes?: ({
|
|
9558
9607
|
type: "motion";
|
|
@@ -9723,6 +9772,7 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
9723
9772
|
push?: {
|
|
9724
9773
|
from?: number | undefined;
|
|
9725
9774
|
to?: number | undefined;
|
|
9775
|
+
origin?: string | undefined;
|
|
9726
9776
|
} | undefined;
|
|
9727
9777
|
code?: string[] | undefined;
|
|
9728
9778
|
crop?: {
|
|
@@ -9744,7 +9794,7 @@ declare const ScenarioSchema: z.ZodEffects<z.ZodObject<{
|
|
|
9744
9794
|
} | undefined;
|
|
9745
9795
|
rate?: number | undefined;
|
|
9746
9796
|
fx?: {
|
|
9747
|
-
kind: "circle" | "arrow";
|
|
9797
|
+
kind: "circle" | "arrow" | "spotlight";
|
|
9748
9798
|
selector?: string | undefined;
|
|
9749
9799
|
delay?: number | undefined;
|
|
9750
9800
|
coords?: number[] | undefined;
|
package/dist/index.js
CHANGED
|
@@ -62,6 +62,12 @@ function buildHideCss(selectors) {
|
|
|
62
62
|
visibility: hidden !important;
|
|
63
63
|
}`;
|
|
64
64
|
}
|
|
65
|
+
function buildMaskCss(selectors) {
|
|
66
|
+
return `${selectors.join(",\n")} {
|
|
67
|
+
filter: blur(10px) !important;
|
|
68
|
+
border-radius: 4px;
|
|
69
|
+
}`;
|
|
70
|
+
}
|
|
65
71
|
function buildCssInjectionScript(css) {
|
|
66
72
|
return `(() => {
|
|
67
73
|
const apply = () => {
|
|
@@ -139,6 +145,9 @@ async function applyPrepare(context, prepare) {
|
|
|
139
145
|
if (prepare.hide.length > 0) {
|
|
140
146
|
cssChunks.push(buildHideCss(prepare.hide));
|
|
141
147
|
}
|
|
148
|
+
if (prepare.mask.length > 0) {
|
|
149
|
+
cssChunks.push(buildMaskCss(prepare.mask));
|
|
150
|
+
}
|
|
142
151
|
if (prepare.inject?.css) {
|
|
143
152
|
const cssFiles = Array.isArray(prepare.inject.css) ? prepare.inject.css : [prepare.inject.css];
|
|
144
153
|
for (const file of cssFiles) {
|
|
@@ -3515,6 +3524,7 @@ var StreamingSession = class extends EventEmitter {
|
|
|
3515
3524
|
// src/scenes/runner.ts
|
|
3516
3525
|
import { chromium as chromium2 } from "playwright";
|
|
3517
3526
|
import { createServer } from "http";
|
|
3527
|
+
import { createHash } from "crypto";
|
|
3518
3528
|
import { execSync } from "child_process";
|
|
3519
3529
|
import { readFile as readFile3, writeFile as writeFile2, mkdtemp, rm as rm2 } from "fs/promises";
|
|
3520
3530
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -3656,21 +3666,21 @@ async function recordFootageTake(scenario, scene, selectors) {
|
|
|
3656
3666
|
const recorder = new ClipwiseRecorder();
|
|
3657
3667
|
const session = await recorder.record(takeScenario);
|
|
3658
3668
|
const renderer = new CanvasRenderer(takeScenario.effects, segmentOutput(scenario), scene.steps);
|
|
3659
|
-
const
|
|
3660
|
-
|
|
3661
|
-
const
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
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
|
+
}
|
|
3668
3678
|
const anchors = [];
|
|
3669
3679
|
for (let k = 0; k < scene.steps.length; k++) {
|
|
3670
3680
|
const idx = session.frames.findIndex((f) => (f.stepIndex ?? 0) >= k);
|
|
3671
3681
|
anchors.push(Math.max(0, idx) / scenario.output.fps);
|
|
3672
3682
|
}
|
|
3673
|
-
return {
|
|
3683
|
+
return { framesDir, count, anchors, boxes };
|
|
3674
3684
|
}
|
|
3675
3685
|
function fitCardW(cw, ch, maxW = 940, maxH = 540) {
|
|
3676
3686
|
return Math.round(Math.min(maxW, maxH * cw / ch));
|
|
@@ -3700,7 +3710,7 @@ async function captureMotionSegment(templateUrl, props, durationMs, scenario) {
|
|
|
3700
3710
|
await browser.close();
|
|
3701
3711
|
return { buffer: await encodeMp4(frames, segmentOutput(scenario)), seconds: totalFrames / fps };
|
|
3702
3712
|
}
|
|
3703
|
-
function vignetteProps(scene, take, serverBase, scenario, brand) {
|
|
3713
|
+
function vignetteProps(scene, take, serverBase, scenario, brand, durMs) {
|
|
3704
3714
|
const W = scenario.viewport.width;
|
|
3705
3715
|
const H = scenario.viewport.height;
|
|
3706
3716
|
let crop = { x: 0, y: 0, w: W, h: H };
|
|
@@ -3724,13 +3734,13 @@ function vignetteProps(scene, take, serverBase, scenario, brand) {
|
|
|
3724
3734
|
const props = {
|
|
3725
3735
|
accent: brand.accent,
|
|
3726
3736
|
font: brand.font,
|
|
3727
|
-
dur:
|
|
3737
|
+
dur: durMs / 1e3,
|
|
3728
3738
|
layout: scene.layout,
|
|
3729
3739
|
num: scene.num ?? "",
|
|
3730
3740
|
label: scene.label ?? "",
|
|
3731
3741
|
caption: scene.caption ?? "",
|
|
3732
3742
|
base: serverBase,
|
|
3733
|
-
count: take.
|
|
3743
|
+
count: take.count,
|
|
3734
3744
|
fps: scenario.output.fps,
|
|
3735
3745
|
start,
|
|
3736
3746
|
rate: scene.rate,
|
|
@@ -3743,13 +3753,19 @@ function vignetteProps(scene, take, serverBase, scenario, brand) {
|
|
|
3743
3753
|
pushTo: scene.push?.to ?? 1
|
|
3744
3754
|
};
|
|
3745
3755
|
if (scene.code?.length) props.code = scene.code.join("||");
|
|
3756
|
+
if (scene.push?.origin) {
|
|
3757
|
+
const box = take.boxes.get(scene.push.origin);
|
|
3758
|
+
if (!box) throw new Error(`vignette push.origin selector "${scene.push.origin}" not found in footage "${scene.footage}"`);
|
|
3759
|
+
props.pushOx = Math.max(0, Math.min(100, (box.x + box.width / 2 - crop.x) / crop.w * 100)).toFixed(1);
|
|
3760
|
+
props.pushOy = Math.max(0, Math.min(100, (box.y + box.height / 2 - crop.y) / crop.h * 100)).toFixed(1);
|
|
3761
|
+
}
|
|
3746
3762
|
if (brand.annotations && scene.fx.length > 0) {
|
|
3747
3763
|
props.fx = scene.fx.map((fx) => {
|
|
3748
3764
|
let coords = fx.coords;
|
|
3749
3765
|
if (fx.selector) {
|
|
3750
3766
|
const box = take.boxes.get(fx.selector);
|
|
3751
3767
|
if (!box) throw new Error(`vignette fx selector "${fx.selector}" not found in footage "${scene.footage}"`);
|
|
3752
|
-
coords = fx.kind === "
|
|
3768
|
+
coords = fx.kind === "arrow" ? [box.x - 160, box.y + 120, box.x - 12, box.y + box.height / 2] : [box.x, box.y, box.width, box.height];
|
|
3753
3769
|
}
|
|
3754
3770
|
return `${fx.kind}@${coords.join(",")}@${fx.delay}`;
|
|
3755
3771
|
}).join(";");
|
|
@@ -3766,6 +3782,7 @@ async function renderScenesTimeline(scenario, scenarioDir, onProgress) {
|
|
|
3766
3782
|
if (scene.type !== "vignette") continue;
|
|
3767
3783
|
const set = selectorsByFootage.get(scene.footage) ?? /* @__PURE__ */ new Set();
|
|
3768
3784
|
if (scene.crop?.selector) set.add(scene.crop.selector);
|
|
3785
|
+
if (scene.push?.origin) set.add(scene.push.origin);
|
|
3769
3786
|
for (const fx of scene.fx) if (fx.selector) set.add(fx.selector);
|
|
3770
3787
|
selectorsByFootage.set(scene.footage, set);
|
|
3771
3788
|
}
|
|
@@ -3781,9 +3798,14 @@ async function renderScenesTimeline(scenario, scenarioDir, onProgress) {
|
|
|
3781
3798
|
const m = req.url?.match(/^\/([\w-]+)\/(\d+)\.png$/);
|
|
3782
3799
|
const take = m ? takes.get(m[1]) : void 0;
|
|
3783
3800
|
if (take) {
|
|
3784
|
-
const idx = Math.min(take.
|
|
3785
|
-
|
|
3786
|
-
|
|
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
|
+
});
|
|
3787
3809
|
} else {
|
|
3788
3810
|
res.writeHead(404);
|
|
3789
3811
|
res.end();
|
|
@@ -3791,27 +3813,32 @@ async function renderScenesTimeline(scenario, scenarioDir, onProgress) {
|
|
|
3791
3813
|
});
|
|
3792
3814
|
await new Promise((r) => server.listen(0, r));
|
|
3793
3815
|
const port = server.address().port;
|
|
3794
|
-
const
|
|
3816
|
+
const beatMs = scenario.audio?.bpm ? 6e4 / scenario.audio.bpm : 0;
|
|
3817
|
+
const durations = timeline.map(
|
|
3818
|
+
(sc) => beatMs ? Math.max(beatMs, Math.round(sc.duration / beatMs) * beatMs) : sc.duration
|
|
3819
|
+
);
|
|
3820
|
+
const totalMs = durations.reduce((s, d) => s + d, 0);
|
|
3795
3821
|
let elapsedMs = 0;
|
|
3796
3822
|
const segments = [];
|
|
3797
3823
|
const tmp = await mkdtemp(join2(tmpdir2(), "clipwise-scenes-"));
|
|
3798
3824
|
try {
|
|
3799
3825
|
for (let i = 0; i < timeline.length; i++) {
|
|
3800
3826
|
const scene = timeline[i];
|
|
3827
|
+
const dur = durations[i];
|
|
3801
3828
|
const label = scene.type === "motion" ? scene.template : `vignette(${scene.footage})`;
|
|
3802
3829
|
onProgress?.({ scene: i + 1, total: timeline.length, label });
|
|
3803
3830
|
const thread = brand.annotations ? {
|
|
3804
3831
|
threadFrom: (elapsedMs / totalMs).toFixed(4),
|
|
3805
|
-
threadTo: ((elapsedMs +
|
|
3832
|
+
threadTo: ((elapsedMs + dur) / totalMs).toFixed(4)
|
|
3806
3833
|
} : {};
|
|
3807
|
-
elapsedMs +=
|
|
3834
|
+
elapsedMs += dur;
|
|
3808
3835
|
let segment;
|
|
3809
3836
|
if (scene.type === "motion") {
|
|
3810
3837
|
const url = resolveMotionTemplate(scene.template, scenarioDir);
|
|
3811
3838
|
segment = await captureMotionSegment(
|
|
3812
3839
|
url,
|
|
3813
|
-
{ accent: brand.accent, font: brand.font, dur:
|
|
3814
|
-
|
|
3840
|
+
{ accent: brand.accent, font: brand.font, dur: dur / 1e3, ...scene.props, ...thread },
|
|
3841
|
+
dur,
|
|
3815
3842
|
scenario
|
|
3816
3843
|
);
|
|
3817
3844
|
} else {
|
|
@@ -3819,8 +3846,8 @@ async function renderScenesTimeline(scenario, scenarioDir, onProgress) {
|
|
|
3819
3846
|
const url = resolveMotionTemplate("vignette", scenarioDir);
|
|
3820
3847
|
segment = await captureMotionSegment(
|
|
3821
3848
|
url,
|
|
3822
|
-
{ ...vignetteProps(scene, take, `http://localhost:${port}/${scene.footage}`, scenario, brand), ...thread },
|
|
3823
|
-
|
|
3849
|
+
{ ...vignetteProps(scene, take, `http://localhost:${port}/${scene.footage}`, scenario, brand, dur), ...thread },
|
|
3850
|
+
dur,
|
|
3824
3851
|
scenario
|
|
3825
3852
|
);
|
|
3826
3853
|
}
|
|
@@ -3834,13 +3861,39 @@ async function renderScenesTimeline(scenario, scenarioDir, onProgress) {
|
|
|
3834
3861
|
const filters = segments.map((_, i) => `[${i}:v]format=yuv420p[v${i}]`).join(";");
|
|
3835
3862
|
const concatInputs = segments.map((_, i) => `[v${i}]`).join("");
|
|
3836
3863
|
const outPath = join2(tmp, "timeline.mp4");
|
|
3864
|
+
let audioInput = "";
|
|
3865
|
+
let audioMap = "";
|
|
3866
|
+
const totalSec = totalMs / 1e3;
|
|
3867
|
+
if (scenario.audio) {
|
|
3868
|
+
const a = scenario.audio;
|
|
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
|
+
}
|
|
3879
|
+
const af = [];
|
|
3880
|
+
if (a.volume !== 1) af.push(`volume=${a.volume}`);
|
|
3881
|
+
if (a.fadeIn > 0) af.push(`afade=t=in:d=${a.fadeIn / 1e3}`);
|
|
3882
|
+
if (a.fadeOut > 0) af.push(`afade=t=out:st=${Math.max(0, totalSec - a.fadeOut / 1e3)}:d=${a.fadeOut / 1e3}`);
|
|
3883
|
+
audioInput = `-stream_loop -1 -i "${audioPath}" `;
|
|
3884
|
+
audioMap = `-map ${segments.length}:a ${af.length ? `-af "${af.join(",")}" ` : ""}-c:a aac -b:a 192k `;
|
|
3885
|
+
}
|
|
3837
3886
|
execSync(
|
|
3838
|
-
`ffmpeg -y ${segments.map((s) => `-i "${s.path}"`).join(" ")} -filter_complex "${filters};${concatInputs}concat=n=${segments.length}:v=1:a=0[v]" -map "[v]" -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}"`,
|
|
3839
3888
|
{ stdio: ["ignore", "ignore", "pipe"] }
|
|
3840
3889
|
);
|
|
3841
3890
|
const buffer = await readFile3(outPath);
|
|
3842
3891
|
await rm2(tmp, { recursive: true, force: true }).catch(() => {
|
|
3843
3892
|
});
|
|
3893
|
+
for (const take of takes.values()) {
|
|
3894
|
+
await rm2(take.framesDir, { recursive: true, force: true }).catch(() => {
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3844
3897
|
return buffer;
|
|
3845
3898
|
}
|
|
3846
3899
|
|
|
@@ -4129,7 +4182,9 @@ var AudioConfigSchema = z.object({
|
|
|
4129
4182
|
/** Fade-in duration in milliseconds. */
|
|
4130
4183
|
fadeIn: z.number().min(0).default(0),
|
|
4131
4184
|
/** Fade-out duration in milliseconds. */
|
|
4132
|
-
fadeOut: z.number().min(0).default(0)
|
|
4185
|
+
fadeOut: z.number().min(0).default(0),
|
|
4186
|
+
/** 트랙 BPM — scenes 타임라인에서 지정 시 신 길이를 비트 격자에 스냅(비트 싱크 컷). */
|
|
4187
|
+
bpm: z.number().min(40).max(220).optional()
|
|
4133
4188
|
});
|
|
4134
4189
|
var AuthConfigSchema = z.object({
|
|
4135
4190
|
/** Path to a Playwright storageState JSON file (cookies + localStorage). */
|
|
@@ -4160,6 +4215,8 @@ var MockRouteSchema = z.object({
|
|
|
4160
4215
|
var PrepareConfigSchema = z.object({
|
|
4161
4216
|
/** 녹화에서 숨길 요소의 CSS 셀렉터 (쿠키 배너, dev 오버레이 등). */
|
|
4162
4217
|
hide: z.array(z.string().min(1)).default([]),
|
|
4218
|
+
/** 블러 처리할 요소의 CSS 셀렉터 (이메일, 금액 등 민감 정보) — 스크롤·이동을 따라간다. */
|
|
4219
|
+
mask: z.array(z.string().min(1)).default([]),
|
|
4163
4220
|
/** Date/Date.now를 이 시각으로 고정 (ISO 8601, 예: "2026-06-10T09:00:00Z"). */
|
|
4164
4221
|
freezeTime: z.string().optional(),
|
|
4165
4222
|
/** Math.random을 이 시드의 결정론적 PRNG로 대체. */
|
|
@@ -4193,7 +4250,7 @@ var ScreenSceneSchema = z.object({
|
|
|
4193
4250
|
steps: z.array(StepSchema).min(1)
|
|
4194
4251
|
});
|
|
4195
4252
|
var SceneFxSchema = z.object({
|
|
4196
|
-
kind: z.enum(["circle", "arrow"]),
|
|
4253
|
+
kind: z.enum(["circle", "arrow", "spotlight"]),
|
|
4197
4254
|
/** 대상 요소 셀렉터 — bounding box를 실측해 좌표로 사용. */
|
|
4198
4255
|
selector: z.string().optional(),
|
|
4199
4256
|
/** 명시 좌표 — circle: [x,y,w,h], arrow: [x1,y1,x2,y2]. */
|
|
@@ -4223,8 +4280,14 @@ var VignetteSceneSchema = z.object({
|
|
|
4223
4280
|
/** 크롭 높이 상한 (px, 원본 기준) — 와이드 스트립 연출용. */
|
|
4224
4281
|
maxH: z.number().optional()
|
|
4225
4282
|
}).optional(),
|
|
4226
|
-
/** 푸시인 카메라 (스케일 from→to).
|
|
4227
|
-
|
|
4283
|
+
/** 푸시인 카메라 (스케일 from→to).
|
|
4284
|
+
* origin: 푸시 중심점 — 셀렉터(실측) 지정 시 그 요소를 향해 밀어 들어가
|
|
4285
|
+
* 다음 신의 크롭과 이어지는 매치컷을 만든다. 기본은 화면 중앙 약간 위. */
|
|
4286
|
+
push: z.object({
|
|
4287
|
+
from: z.number().default(1),
|
|
4288
|
+
to: z.number().default(1),
|
|
4289
|
+
origin: z.string().optional()
|
|
4290
|
+
}).optional(),
|
|
4228
4291
|
/** 푸티지 인용 시작점 — 초(number) 또는 step 경계 anchor. */
|
|
4229
4292
|
start: z.union([z.number(), z.object({ step: z.number().int().min(0), offset: z.number().default(0) })]).default(0),
|
|
4230
4293
|
/** 푸티지 재생 배속. */
|
package/package.json
CHANGED
package/skills/clipwise.md
CHANGED
|
@@ -375,12 +375,14 @@ scenes:
|
|
|
375
375
|
label: "Smart Speed"
|
|
376
376
|
caption: "Loading compressed, *results crisp*"
|
|
377
377
|
crop: { selector: ".panel", pad: 14, maxH: 250 } # selector-measured, never guess pixels
|
|
378
|
-
push: { from: 1.05, to: 1 }
|
|
378
|
+
push: { from: 1.05, to: 1, origin: ".panel" } # origin: match-cut — push toward
|
|
379
|
+
# a selector so the NEXT scene's crop continues the move
|
|
379
380
|
start: { step: 3, offset: 0 } # quote footage from a step boundary (or seconds)
|
|
380
381
|
rate: 1.15 # playback speed of the quoted footage
|
|
381
|
-
fx: #
|
|
382
|
-
- { kind: circle, selector: "#revenue", delay: 2500 }
|
|
383
|
-
- { kind: arrow, selector: ".panel", delay: 2900 }
|
|
382
|
+
fx: # annotations on the footage
|
|
383
|
+
- { kind: circle, selector: "#revenue", delay: 2500 } # hand-drawn circle
|
|
384
|
+
- { kind: arrow, selector: ".panel", delay: 2900 } # drawn arrow
|
|
385
|
+
- { kind: spotlight, selector: "#revenue", delay: 2400 } # dim everything else
|
|
384
386
|
# code: ["prepare:", " hide: [...]"] # split layout left code card
|
|
385
387
|
```
|
|
386
388
|
|
|
@@ -395,6 +397,13 @@ scenes:
|
|
|
395
397
|
5. **Footage effects**: in scenes mode set only `cursor:` (highlight: false) — zoom/frame/background
|
|
396
398
|
are handled by the vignette compositor, not the recorder
|
|
397
399
|
6. Keep one screen take (~12-15s) and let vignettes quote segments via `start: { step: N }`
|
|
400
|
+
7. **Sensitive data**: `prepare.mask: [".email", ".amount"]` blurs elements at record time
|
|
401
|
+
(follows scrolling — never ask the user to fake their data)
|
|
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).
|
|
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
|
|
398
407
|
|
|
399
408
|
## Critical Rules
|
|
400
409
|
|
|
@@ -113,6 +113,15 @@
|
|
|
113
113
|
/* ── 선 드로잉 주석 레이어 (fx=circle@…;arrow@…) — 카드 위에 그려짐 ── */
|
|
114
114
|
.annotations { position: absolute; inset: 0; z-index: 5; pointer-events: none;
|
|
115
115
|
overflow: visible; }
|
|
116
|
+
/* 스포트라이트 — 대상만 남기고 주변을 디밍 (fx kind: spotlight) */
|
|
117
|
+
.spotlight { position: absolute; z-index: 4; pointer-events: none;
|
|
118
|
+
/* spread가 너무 크면 Chromium이 그림자를 클램프하므로 카드 크기 수준으로 */
|
|
119
|
+
box-shadow: 0 0 0 1400px rgba(12, 12, 16, 0.52); border-radius: 10px;
|
|
120
|
+
/* 다크 UI에서도 컷아웃이 인지되도록 은은한 악센트 림 */
|
|
121
|
+
outline: 1.5px solid color-mix(in srgb, var(--accent) 55%, transparent);
|
|
122
|
+
outline-offset: 3px;
|
|
123
|
+
opacity: 0; animation: spot-in 550ms cubic-bezier(0.4, 0, 0.2, 1) both; }
|
|
124
|
+
@keyframes spot-in { to { opacity: 1; } }
|
|
116
125
|
.annotations .stroke { stroke: var(--accent); fill: none; stroke-width: 3.5;
|
|
117
126
|
stroke-linecap: round; stroke-linejoin: round;
|
|
118
127
|
stroke-dasharray: 1; stroke-dashoffset: 1;
|
|
@@ -178,9 +187,12 @@
|
|
|
178
187
|
card.style.setProperty("--push-from", P.get("pushFrom") || "1");
|
|
179
188
|
card.style.setProperty("--push-to", P.get("pushTo") || "1");
|
|
180
189
|
const fullCrop = cw >= SRC_W;
|
|
190
|
+
// push origin — 매치컷: 다음 신의 크롭 중심을 향해 밀어 들어가도록 중심점 지정
|
|
191
|
+
const pushOx = num("pushOx", 50), pushOy = num("pushOy", 42);
|
|
181
192
|
card.innerHTML = `
|
|
182
193
|
${fullCrop ? '<div class="titlebar"><i></i><i></i><i></i></div>' : ""}
|
|
183
|
-
<div class="viewport"><div class="pusher"
|
|
194
|
+
<div class="viewport"><div class="pusher"
|
|
195
|
+
style="animation-duration:${num("dur", 4)}s; transform-origin:${pushOx}% ${pushOy}%">
|
|
184
196
|
<img class="footage" id="footage"
|
|
185
197
|
style="width:${SRC_W * scale}px;height:${SRC_H * scale}px;left:${-cx * scale}px;top:${-cy * scale}px" />
|
|
186
198
|
</div></div>`;
|
|
@@ -224,6 +236,15 @@
|
|
|
224
236
|
for (const spec of fxSpecs) {
|
|
225
237
|
const [kind, coords, delay = "0"] = spec.split("@");
|
|
226
238
|
const n = coords.split(",").map(Number);
|
|
239
|
+
if (kind === "spotlight") {
|
|
240
|
+
// 푸시 레이어 안에 디밍 컷아웃 — 푸티지에 앵커링되어 카메라를 따라간다
|
|
241
|
+
const [sx, sy] = toCard(n[0], n[1]);
|
|
242
|
+
const spot = document.createElement("div");
|
|
243
|
+
spot.className = "spotlight";
|
|
244
|
+
spot.style.cssText = `left:${sx - 6}px;top:${sy - 6}px;width:${n[2] * effScale + 12}px;height:${n[3] * effScale + 12}px;animation-delay:${delay}ms`;
|
|
245
|
+
card.querySelector(".pusher").appendChild(spot);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
227
248
|
if (kind === "circle") {
|
|
228
249
|
// 손그림 느낌: 살짝 큰 타원 + 미세 회전
|
|
229
250
|
const [x0, y0] = toCard(n[0], n[1]);
|