clipwise 0.1.0 → 0.1.2
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/README.ko.md +264 -0
- package/README.md +17 -4
- package/dist/cli/index.js +13 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +13 -1
- package/package.json +1 -1
package/README.ko.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
[English](./README.md) | [한국어](./README.ko.md)
|
|
2
|
+
|
|
3
|
+
# Clipwise
|
|
4
|
+
|
|
5
|
+
YAML 시나리오를 작성하면 시네마틱 데모 영상(MP4/GIF)을 자동으로 만들어주는 스크린 레코더. Playwright CDP 기반.
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="https://github.com/kwakseongjae/clipwise/releases/download/v0.1.0/demo.gif" width="100%" alt="Clipwise Demo" />
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
> *`npx clipwise demo` 한 줄로 생성된 영상입니다.*
|
|
12
|
+
|
|
13
|
+
## 빠른 시작
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# 설치
|
|
17
|
+
npm install -D clipwise
|
|
18
|
+
|
|
19
|
+
# 내장 데모 즉시 실행
|
|
20
|
+
npx clipwise demo
|
|
21
|
+
|
|
22
|
+
# 또는 직접 시나리오 작성
|
|
23
|
+
npx clipwise init # clipwise.yaml 템플릿 생성
|
|
24
|
+
# clipwise.yaml 편집 — URL을 내 사이트로 변경
|
|
25
|
+
npx clipwise record clipwise.yaml -f mp4 # 녹화!
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 요구사항
|
|
29
|
+
|
|
30
|
+
- **Node.js** >= 18
|
|
31
|
+
- **ffmpeg** (MP4 출력용)
|
|
32
|
+
- **Chromium** (첫 실행 시 Playwright가 자동 설치)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# macOS
|
|
36
|
+
brew install ffmpeg
|
|
37
|
+
|
|
38
|
+
# Ubuntu
|
|
39
|
+
sudo apt install ffmpeg
|
|
40
|
+
|
|
41
|
+
# Windows
|
|
42
|
+
choco install ffmpeg
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 사용법
|
|
46
|
+
|
|
47
|
+
### CLI 명령어
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# 즉시 데모 — 내장 대시보드 녹화
|
|
51
|
+
npx clipwise demo # 브라우저 프레임, MP4
|
|
52
|
+
npx clipwise demo --device iphone # iPhone 목업
|
|
53
|
+
npx clipwise demo --device android # Android 목업
|
|
54
|
+
npx clipwise demo --device ipad # iPad 목업
|
|
55
|
+
npx clipwise demo --url https://my-app.com # 내 사이트 데모
|
|
56
|
+
|
|
57
|
+
# YAML 시나리오로 녹화
|
|
58
|
+
npx clipwise record <scenario.yaml> -f mp4 -o ./output
|
|
59
|
+
npx clipwise record <scenario.yaml> -f gif -o ./output
|
|
60
|
+
|
|
61
|
+
# 템플릿 초기화
|
|
62
|
+
npx clipwise init
|
|
63
|
+
|
|
64
|
+
# 녹화 없이 검증만
|
|
65
|
+
npx clipwise validate <scenario.yaml>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 프로그래밍 API
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { ClipwiseRecorder, CanvasRenderer, encodeMp4, loadScenario } from "clipwise";
|
|
72
|
+
|
|
73
|
+
const scenario = await loadScenario("my-scenario.yaml");
|
|
74
|
+
const recorder = new ClipwiseRecorder();
|
|
75
|
+
const session = await recorder.record(scenario);
|
|
76
|
+
|
|
77
|
+
const renderer = new CanvasRenderer(scenario.effects, scenario.output, scenario.steps);
|
|
78
|
+
const frames = await renderer.composeAll(session.frames);
|
|
79
|
+
|
|
80
|
+
const mp4 = await encodeMp4(frames, scenario.output);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## YAML 시나리오 형식
|
|
84
|
+
|
|
85
|
+
시나리오는 4개 섹션으로 구성됩니다: 메타데이터, 이펙트, 출력, 스텝.
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
name: "My Demo"
|
|
89
|
+
description: "선택 설명"
|
|
90
|
+
|
|
91
|
+
viewport:
|
|
92
|
+
width: 1280 # 브라우저 너비 (기본: 1280)
|
|
93
|
+
height: 800 # 브라우저 높이 (기본: 800)
|
|
94
|
+
|
|
95
|
+
effects:
|
|
96
|
+
# 아래 "이펙트" 섹션 참조
|
|
97
|
+
|
|
98
|
+
output:
|
|
99
|
+
format: mp4 # gif | mp4 | png-sequence
|
|
100
|
+
width: 1280
|
|
101
|
+
height: 800
|
|
102
|
+
fps: 30 # 1-60
|
|
103
|
+
quality: 80 # 1-100 (MP4: CRF에 매핑)
|
|
104
|
+
|
|
105
|
+
steps:
|
|
106
|
+
- name: "스텝 이름"
|
|
107
|
+
actions:
|
|
108
|
+
- action: navigate
|
|
109
|
+
url: "https://example.com"
|
|
110
|
+
captureDelay: 200 # 액션 후 대기(ms)
|
|
111
|
+
holdDuration: 800 # 결과 화면 유지(ms)
|
|
112
|
+
transition: none # none | fade
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 액션
|
|
116
|
+
|
|
117
|
+
| 액션 | 파라미터 | 설명 |
|
|
118
|
+
|------|---------|------|
|
|
119
|
+
| `navigate` | `url`, `waitUntil?` | URL로 이동 |
|
|
120
|
+
| `click` | `selector`, `delay?` | 요소 클릭 |
|
|
121
|
+
| `type` | `selector`, `text`, `delay?` | 텍스트 입력 (한 글자씩) |
|
|
122
|
+
| `hover` | `selector` | 요소에 마우스 올리기 |
|
|
123
|
+
| `scroll` | `y?`, `x?`, `selector?`, `smooth?` | 스크롤 |
|
|
124
|
+
| `wait` | `duration` | 대기 (ms) |
|
|
125
|
+
| `screenshot` | `name?`, `fullPage?` | 캡처 마커 |
|
|
126
|
+
|
|
127
|
+
### 타이밍 팁
|
|
128
|
+
|
|
129
|
+
빠른 데모 (~30초):
|
|
130
|
+
- `captureDelay: 50-100` ms
|
|
131
|
+
- `holdDuration: 500-800` ms
|
|
132
|
+
- `type.delay: 15-25` ms/글자
|
|
133
|
+
|
|
134
|
+
느린 시네마틱:
|
|
135
|
+
- `captureDelay: 200-400` ms
|
|
136
|
+
- `holdDuration: 1500-2500` ms
|
|
137
|
+
- `type.delay: 40-60` ms/글자
|
|
138
|
+
|
|
139
|
+
## 이펙트
|
|
140
|
+
|
|
141
|
+
모든 이펙트는 선택사항이며 합리적인 기본값이 있습니다.
|
|
142
|
+
|
|
143
|
+
### 줌
|
|
144
|
+
|
|
145
|
+
적응형 줌 — 커서를 따라가며 클릭 대상에 자동 줌인.
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
zoom:
|
|
149
|
+
enabled: true
|
|
150
|
+
scale: 1.8 # 최대 줌 (1-5)
|
|
151
|
+
duration: 500 # 애니메이션 ms
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 커서
|
|
155
|
+
|
|
156
|
+
커스텀 커서 + 클릭 리플 + 트레일 + 하이라이트 + 속도 조절.
|
|
157
|
+
|
|
158
|
+
```yaml
|
|
159
|
+
cursor:
|
|
160
|
+
enabled: true
|
|
161
|
+
size: 20
|
|
162
|
+
speed: "fast" # fast (~72ms) | normal (~144ms) | slow (~288ms)
|
|
163
|
+
clickEffect: true
|
|
164
|
+
trail: true
|
|
165
|
+
highlight: true
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 배경
|
|
169
|
+
|
|
170
|
+
그라디언트/단색 패딩 + 라운드 코너 + 그림자.
|
|
171
|
+
|
|
172
|
+
```yaml
|
|
173
|
+
background:
|
|
174
|
+
type: gradient
|
|
175
|
+
value: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
|
|
176
|
+
padding: 48
|
|
177
|
+
borderRadius: 14
|
|
178
|
+
shadow: true
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 디바이스 프레임
|
|
182
|
+
|
|
183
|
+
녹화를 디바이스 목업으로 감싸기.
|
|
184
|
+
|
|
185
|
+
```yaml
|
|
186
|
+
deviceFrame:
|
|
187
|
+
enabled: true
|
|
188
|
+
type: browser # browser | iphone | ipad | android | none
|
|
189
|
+
darkMode: true
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
| 타입 | 설명 |
|
|
193
|
+
|------|------|
|
|
194
|
+
| `browser` | macOS 브라우저 크롬 (신호등 버튼) |
|
|
195
|
+
| `iphone` | iPhone 15 Pro (Dynamic Island + 홈바) |
|
|
196
|
+
| `ipad` | iPad Pro (전면 카메라) |
|
|
197
|
+
| `android` | Android (펀치홀 카메라) |
|
|
198
|
+
|
|
199
|
+
### 키스트로크 HUD
|
|
200
|
+
|
|
201
|
+
타이핑 내용을 화면 하단에 표시.
|
|
202
|
+
|
|
203
|
+
```yaml
|
|
204
|
+
keystroke:
|
|
205
|
+
enabled: true
|
|
206
|
+
position: bottom-center
|
|
207
|
+
fontSize: 16
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### 워터마크
|
|
211
|
+
|
|
212
|
+
코너에 텍스트 오버레이.
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
watermark:
|
|
216
|
+
enabled: true
|
|
217
|
+
text: "Clipwise"
|
|
218
|
+
position: bottom-right
|
|
219
|
+
opacity: 0.5
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### 속도 램프
|
|
223
|
+
|
|
224
|
+
클릭 근처 슬로우모션, 유휴 구간 빨리감기.
|
|
225
|
+
|
|
226
|
+
```yaml
|
|
227
|
+
speedRamp:
|
|
228
|
+
enabled: true
|
|
229
|
+
idleSpeed: 3.0
|
|
230
|
+
actionSpeed: 0.8
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## AI로 시나리오 작성
|
|
234
|
+
|
|
235
|
+
[PROMPTS.md](./PROMPTS.md)에 바로 사용할 수 있는 AI 프롬프트 템플릿이 있습니다. ChatGPT나 Claude에 복붙하고 내 사이트 URL만 넣으면 YAML 시나리오를 생성해줍니다.
|
|
236
|
+
|
|
237
|
+
## GitHub Pages
|
|
238
|
+
|
|
239
|
+
`docs/` 폴더에 문서 사이트와 라이브 데모 대시보드가 포함되어 있습니다:
|
|
240
|
+
|
|
241
|
+
1. GitHub에 push: `git push origin main`
|
|
242
|
+
2. **Settings > Pages** > source: `main`, folder: `/docs`
|
|
243
|
+
3. 문서: `https://username.github.io/clipwise/`
|
|
244
|
+
4. 데모: `https://username.github.io/clipwise/demo/`
|
|
245
|
+
|
|
246
|
+
## 보안
|
|
247
|
+
|
|
248
|
+
- **셀렉터 검증**: YAML의 CSS 셀렉터는 안전한 문자만 허용
|
|
249
|
+
- **URL 처리**: `http://`, `https://`, `file://` 스키마만 허용
|
|
250
|
+
- **Chromium 샌드박스**: Playwright 기본 샌드박싱 적용
|
|
251
|
+
- **로컬 처리**: 녹화 프레임은 절대 외부로 전송되지 않음
|
|
252
|
+
|
|
253
|
+
## 개발
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
npm install # 의존성 설치
|
|
257
|
+
npm run build # tsup으로 빌드
|
|
258
|
+
npm run typecheck # 타입 체크
|
|
259
|
+
npm test # 테스트 (vitest)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## 라이선스
|
|
263
|
+
|
|
264
|
+
MIT
|
package/README.md
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
[English](./README.md) | [한국어](./README.ko.md)
|
|
2
|
+
|
|
1
3
|
# Clipwise
|
|
2
4
|
|
|
3
5
|
Scriptable cinematic screen recorder for product demos — YAML in, polished MP4 out. Powered by Playwright CDP.
|
|
4
6
|
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="https://github.com/kwakseongjae/clipwise/releases/download/v0.1.0/demo.gif" width="100%" alt="Clipwise Demo" />
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
> *Generated with `npx clipwise demo` — zero config, one command.*
|
|
12
|
+
|
|
5
13
|
## Quick Start
|
|
6
14
|
|
|
7
15
|
```bash
|
|
@@ -323,19 +331,24 @@ npx clipwise record my-scenario.yaml -f mp4 -o ./output
|
|
|
323
331
|
- Keep `type.delay` at 15-25ms for a fast but readable typing effect
|
|
324
332
|
- Use `transition: fade` between major sections for cinematic cuts
|
|
325
333
|
|
|
326
|
-
|
|
334
|
+
### Writing Scenarios with AI
|
|
335
|
+
|
|
336
|
+
See [PROMPTS.md](./PROMPTS.md) for a ready-to-use prompt template. Copy-paste it to ChatGPT or Claude with your site URL, and get a working YAML scenario back.
|
|
337
|
+
|
|
338
|
+
## GitHub Pages
|
|
327
339
|
|
|
328
|
-
Clipwise includes a demo dashboard in `docs
|
|
340
|
+
Clipwise includes a documentation site and a live demo dashboard in the `docs/` folder. To host it:
|
|
329
341
|
|
|
330
342
|
1. Push to GitHub: `git push origin main`
|
|
331
343
|
2. Go to **Settings > Pages**
|
|
332
344
|
3. Set source to **Deploy from a branch**, select `main`, folder `/docs`
|
|
333
|
-
4.
|
|
345
|
+
4. Docs go live at `https://kwakseongjae.github.io/clipwise/`
|
|
346
|
+
5. Demo dashboard at `https://kwakseongjae.github.io/clipwise/demo/`
|
|
334
347
|
|
|
335
348
|
Then anyone can record the demo site:
|
|
336
349
|
|
|
337
350
|
```bash
|
|
338
|
-
npx clipwise demo --url https://kwakseongjae.github.io/clipwise/
|
|
351
|
+
npx clipwise demo --url https://kwakseongjae.github.io/clipwise/demo/
|
|
339
352
|
```
|
|
340
353
|
|
|
341
354
|
## Security
|
package/dist/cli/index.js
CHANGED
|
@@ -373,6 +373,7 @@ var ClipwiseRecorder = class {
|
|
|
373
373
|
isCapturing = false;
|
|
374
374
|
targetFps = 30;
|
|
375
375
|
cursorSpeed = "fast";
|
|
376
|
+
firstContentTimestamp = 0;
|
|
376
377
|
/**
|
|
377
378
|
* Launch the browser and create a page with the scenario viewport.
|
|
378
379
|
*/
|
|
@@ -395,6 +396,7 @@ var ClipwiseRecorder = class {
|
|
|
395
396
|
this.currentStepIndex = 0;
|
|
396
397
|
this.cursorPosition = { x: 0, y: 0 };
|
|
397
398
|
this.isCapturing = false;
|
|
399
|
+
this.firstContentTimestamp = 0;
|
|
398
400
|
}
|
|
399
401
|
/**
|
|
400
402
|
* Start CDP screencast for continuous frame capture.
|
|
@@ -534,6 +536,13 @@ var ClipwiseRecorder = class {
|
|
|
534
536
|
switch (action.action) {
|
|
535
537
|
case "navigate": {
|
|
536
538
|
await this.page.goto(action.url, { waitUntil: action.waitUntil });
|
|
539
|
+
await this.page.evaluate(
|
|
540
|
+
() => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)))
|
|
541
|
+
).catch(() => {
|
|
542
|
+
});
|
|
543
|
+
if (this.firstContentTimestamp === 0) {
|
|
544
|
+
this.firstContentTimestamp = Date.now();
|
|
545
|
+
}
|
|
537
546
|
await this.waitWithRepaints(300);
|
|
538
547
|
break;
|
|
539
548
|
}
|
|
@@ -654,7 +663,10 @@ var ClipwiseRecorder = class {
|
|
|
654
663
|
*/
|
|
655
664
|
buildCapturedFrames() {
|
|
656
665
|
if (this.rawFrames.length === 0) return [];
|
|
657
|
-
|
|
666
|
+
const contentStart = this.firstContentTimestamp;
|
|
667
|
+
const trimmed = contentStart > 0 ? this.rawFrames.filter((f) => f.timestamp >= contentStart) : this.rawFrames;
|
|
668
|
+
if (trimmed.length === 0) return [];
|
|
669
|
+
return trimmed.map((raw, index) => {
|
|
658
670
|
const cursorPos = this.interpolateCursorAt(raw.timestamp);
|
|
659
671
|
const clickEvent = this.clickTimeline.find(
|
|
660
672
|
(click) => raw.timestamp >= click.timestamp && raw.timestamp <= click.timestamp + CLICK_EFFECT_DURATION_MS
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -76,6 +76,7 @@ var ClipwiseRecorder = class {
|
|
|
76
76
|
isCapturing = false;
|
|
77
77
|
targetFps = 30;
|
|
78
78
|
cursorSpeed = "fast";
|
|
79
|
+
firstContentTimestamp = 0;
|
|
79
80
|
/**
|
|
80
81
|
* Launch the browser and create a page with the scenario viewport.
|
|
81
82
|
*/
|
|
@@ -98,6 +99,7 @@ var ClipwiseRecorder = class {
|
|
|
98
99
|
this.currentStepIndex = 0;
|
|
99
100
|
this.cursorPosition = { x: 0, y: 0 };
|
|
100
101
|
this.isCapturing = false;
|
|
102
|
+
this.firstContentTimestamp = 0;
|
|
101
103
|
}
|
|
102
104
|
/**
|
|
103
105
|
* Start CDP screencast for continuous frame capture.
|
|
@@ -237,6 +239,13 @@ var ClipwiseRecorder = class {
|
|
|
237
239
|
switch (action.action) {
|
|
238
240
|
case "navigate": {
|
|
239
241
|
await this.page.goto(action.url, { waitUntil: action.waitUntil });
|
|
242
|
+
await this.page.evaluate(
|
|
243
|
+
() => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)))
|
|
244
|
+
).catch(() => {
|
|
245
|
+
});
|
|
246
|
+
if (this.firstContentTimestamp === 0) {
|
|
247
|
+
this.firstContentTimestamp = Date.now();
|
|
248
|
+
}
|
|
240
249
|
await this.waitWithRepaints(300);
|
|
241
250
|
break;
|
|
242
251
|
}
|
|
@@ -357,7 +366,10 @@ var ClipwiseRecorder = class {
|
|
|
357
366
|
*/
|
|
358
367
|
buildCapturedFrames() {
|
|
359
368
|
if (this.rawFrames.length === 0) return [];
|
|
360
|
-
|
|
369
|
+
const contentStart = this.firstContentTimestamp;
|
|
370
|
+
const trimmed = contentStart > 0 ? this.rawFrames.filter((f) => f.timestamp >= contentStart) : this.rawFrames;
|
|
371
|
+
if (trimmed.length === 0) return [];
|
|
372
|
+
return trimmed.map((raw, index) => {
|
|
361
373
|
const cursorPos = this.interpolateCursorAt(raw.timestamp);
|
|
362
374
|
const clickEvent = this.clickTimeline.find(
|
|
363
375
|
(click) => raw.timestamp >= click.timestamp && raw.timestamp <= click.timestamp + CLICK_EFFECT_DURATION_MS
|