@su-record/vibe 2.7.6 → 2.7.9
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/commands/init.d.ts +10 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +78 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +17 -2
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/postinstall/codex-agents.d.ts +12 -0
- package/dist/cli/postinstall/codex-agents.d.ts.map +1 -0
- package/dist/cli/postinstall/codex-agents.js +51 -0
- package/dist/cli/postinstall/codex-agents.js.map +1 -0
- package/dist/cli/postinstall/codex-instruction.d.ts +10 -0
- package/dist/cli/postinstall/codex-instruction.d.ts.map +1 -0
- package/dist/cli/postinstall/codex-instruction.js +56 -0
- package/dist/cli/postinstall/codex-instruction.js.map +1 -0
- package/dist/cli/postinstall/constants.d.ts.map +1 -1
- package/dist/cli/postinstall/constants.js +1 -0
- package/dist/cli/postinstall/constants.js.map +1 -1
- package/dist/cli/postinstall/gemini-agents.d.ts +12 -0
- package/dist/cli/postinstall/gemini-agents.d.ts.map +1 -0
- package/dist/cli/postinstall/gemini-agents.js +80 -0
- package/dist/cli/postinstall/gemini-agents.js.map +1 -0
- package/dist/cli/postinstall/gemini-instruction.d.ts +10 -0
- package/dist/cli/postinstall/gemini-instruction.d.ts.map +1 -0
- package/dist/cli/postinstall/gemini-instruction.js +59 -0
- package/dist/cli/postinstall/gemini-instruction.js.map +1 -0
- package/dist/cli/postinstall/index.d.ts +4 -0
- package/dist/cli/postinstall/index.d.ts.map +1 -1
- package/dist/cli/postinstall/index.js +4 -0
- package/dist/cli/postinstall/index.js.map +1 -1
- package/dist/cli/postinstall/main.d.ts.map +1 -1
- package/dist/cli/postinstall/main.js +34 -1
- package/dist/cli/postinstall/main.js.map +1 -1
- package/dist/cli/postinstall.d.ts +1 -1
- package/dist/cli/postinstall.d.ts.map +1 -1
- package/dist/cli/postinstall.js +1 -1
- package/dist/cli/postinstall.js.map +1 -1
- package/dist/cli/setup/ProjectSetup.d.ts +15 -0
- package/dist/cli/setup/ProjectSetup.d.ts.map +1 -1
- package/dist/cli/setup/ProjectSetup.js +159 -0
- package/dist/cli/setup/ProjectSetup.js.map +1 -1
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +1 -1
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/utils/cli-detector.d.ts +25 -0
- package/dist/cli/utils/cli-detector.d.ts.map +1 -0
- package/dist/cli/utils/cli-detector.js +55 -0
- package/dist/cli/utils/cli-detector.js.map +1 -0
- package/hooks/gemini-hooks.json +73 -0
- package/package.json +1 -1
- package/skills/agents-md/SKILL.md +120 -0
- package/skills/brand-assets/SKILL.md +8 -0
- package/skills/characterization-test/SKILL.md +4 -0
- package/skills/commerce-patterns/SKILL.md +36 -338
- package/skills/commit-push-pr/SKILL.md +21 -64
- package/skills/core-capabilities/SKILL.md +26 -142
- package/skills/e2e-commerce/SKILL.md +37 -284
- package/skills/frontend-design/SKILL.md +12 -31
- package/skills/git-worktree/SKILL.md +34 -146
- package/skills/handoff/SKILL.md +8 -0
- package/skills/parallel-research/SKILL.md +7 -0
- package/skills/priority-todos/SKILL.md +34 -213
- package/skills/seo-checklist/SKILL.md +38 -225
- package/skills/tool-fallback/SKILL.md +53 -143
- package/skills/typescript-advanced-types/SKILL.md +30 -685
- package/skills/ui-ux-pro-max/SKILL.md +40 -220
- package/skills/vercel-react-best-practices/SKILL.md +38 -283
- package/skills/video-production/SKILL.md +35 -206
|
@@ -1,304 +1,59 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vercel-react-best-practices
|
|
3
|
-
description: "React/Next.js performance
|
|
3
|
+
description: "React/Next.js performance gotchas from Vercel engineering. Non-intuitive pitfalls that LLMs commonly miss."
|
|
4
4
|
triggers: [react, next.js, performance, optimization, vercel, component, rendering]
|
|
5
5
|
priority: 60
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Vercel React Best Practices
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## Pre-check (K1)
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
> Is this a React/Next.js performance issue? Standard React patterns (useState, useEffect, components) don't need this skill. Activate only for performance optimization or code review.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
- When implementing data fetching (client/server)
|
|
16
|
-
- During performance-focused code reviews
|
|
17
|
-
- When refactoring existing React/Next.js code
|
|
18
|
-
- When optimizing bundle size or load times
|
|
14
|
+
## CRITICAL Gotchas
|
|
19
15
|
|
|
20
|
-
|
|
16
|
+
### Waterfall Elimination
|
|
21
17
|
|
|
22
|
-
|
|
|
23
|
-
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
| 4 | Client Data Fetching | MEDIUM-HIGH | `client-` |
|
|
28
|
-
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
|
|
29
|
-
| 6 | Rendering Performance | MEDIUM | `rendering-` |
|
|
30
|
-
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
|
|
31
|
-
| 8 | Advanced Patterns | LOW | `advanced-` |
|
|
18
|
+
| Gotcha | Why Non-obvious |
|
|
19
|
+
|--------|----------------|
|
|
20
|
+
| **Sequential awaits** | `const a = await f1(); const b = await f2();` creates waterfall. Use `Promise.all([f1(), f2()])` for independent ops |
|
|
21
|
+
| **Await placement** | Move `await` to the branch where value is actually used, not at declaration |
|
|
22
|
+
| **Missing Suspense** | Wrap slow server components in `<Suspense>` to stream — don't block entire page |
|
|
32
23
|
|
|
33
|
-
|
|
24
|
+
### Bundle Size
|
|
34
25
|
|
|
35
|
-
|
|
26
|
+
| Gotcha | Why Non-obvious |
|
|
27
|
+
|--------|----------------|
|
|
28
|
+
| **Barrel imports** | `import { Button } from "@/components"` pulls entire barrel. Use `import { Button } from "@/components/Button"` |
|
|
29
|
+
| **Third-party in initial bundle** | Load analytics/logging AFTER hydration with `next/dynamic` or lazy `useEffect` |
|
|
30
|
+
| **Heavy components** | Charts, editors, maps → `next/dynamic` with `{ ssr: false }` |
|
|
36
31
|
|
|
37
|
-
|
|
38
|
-
|------|-------------|
|
|
39
|
-
| `async-defer-await` | Move await to the branch where the value is actually used |
|
|
40
|
-
| `async-parallel` | Use `Promise.all()` for independent operations |
|
|
41
|
-
| `async-dependencies` | Use better-all for partial dependencies |
|
|
42
|
-
| `async-api-routes` | Start Promises early, await late |
|
|
43
|
-
| `async-suspense-boundaries` | Stream content with Suspense |
|
|
32
|
+
## HIGH Gotchas
|
|
44
33
|
|
|
45
|
-
|
|
34
|
+
### Server-side
|
|
46
35
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
36
|
+
| Gotcha | Fix |
|
|
37
|
+
|--------|-----|
|
|
38
|
+
| Duplicate DB calls across server components | Wrap with `React.cache()` for per-request dedup |
|
|
39
|
+
| Large data serialized to client | Pick only needed fields before passing to client components |
|
|
40
|
+
| Blocking post-processing (logging, analytics) | Use `after()` for non-blocking tasks |
|
|
52
41
|
|
|
53
|
-
|
|
54
|
-
const [user, posts, comments] = await Promise.all([
|
|
55
|
-
getUser(id),
|
|
56
|
-
getPosts(id),
|
|
57
|
-
getComments(id),
|
|
58
|
-
]);
|
|
59
|
-
```
|
|
42
|
+
## MEDIUM Gotchas
|
|
60
43
|
|
|
61
|
-
|
|
44
|
+
| Gotcha | Fix |
|
|
45
|
+
|--------|-----|
|
|
46
|
+
| Expensive computation re-runs on parent re-render | Isolate in `memo()` wrapped component |
|
|
47
|
+
| Static JSX recreated every render | Hoist outside component: `const HEADER = <header>...</header>` |
|
|
48
|
+
| Long lists render all items | `content-visibility: auto; contain-intrinsic-size: 0 80px;` on list items |
|
|
49
|
+
| `{count && <Item />}` renders `0` | Use ternary: `{count > 0 ? <Item /> : null}` |
|
|
50
|
+
| Event handler changes every render → effect re-runs | Store handlers in `useRef` for stable effects |
|
|
51
|
+
| Object in useEffect deps | Use primitive values (id, not entire object) as dependencies |
|
|
62
52
|
|
|
63
|
-
|
|
64
|
-
|------|-------------|
|
|
65
|
-
| `bundle-barrel-imports` | Avoid barrel files, use direct imports |
|
|
66
|
-
| `bundle-dynamic-imports` | Use `next/dynamic` for heavy components |
|
|
67
|
-
| `bundle-defer-third-party` | Load analytics/logging after hydration |
|
|
68
|
-
| `bundle-conditional` | Load modules only when feature is enabled |
|
|
69
|
-
| `bundle-preload` | Preload on hover/focus for perceived speed |
|
|
53
|
+
## Done Criteria (K4)
|
|
70
54
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// ✅ Good: direct import → tree-shakeable
|
|
78
|
-
import { Button } from "@/components/Button";
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### 3. Server-side Performance (HIGH)
|
|
82
|
-
|
|
83
|
-
| Rule | Description |
|
|
84
|
-
|------|-------------|
|
|
85
|
-
| `server-cache-react` | Deduplicate per-request with `React.cache()` |
|
|
86
|
-
| `server-cache-lru` | Cross-request caching with LRU cache |
|
|
87
|
-
| `server-serialization` | Minimize data passed to client components |
|
|
88
|
-
| `server-parallel-fetching` | Redesign component structure for parallel fetching |
|
|
89
|
-
| `server-after-nonblocking` | Non-blocking post-processing with `after()` |
|
|
90
|
-
|
|
91
|
-
**Key Example — `server-cache-react` (HIGH):**
|
|
92
|
-
|
|
93
|
-
```typescript
|
|
94
|
-
// ❌ Bad: duplicate DB calls within same request
|
|
95
|
-
async function Layout() {
|
|
96
|
-
const user = await getUser(); // call 1
|
|
97
|
-
return <Header user={user}><Content /></Header>;
|
|
98
|
-
}
|
|
99
|
-
async function Content() {
|
|
100
|
-
const user = await getUser(); // call 2 (duplicate)
|
|
101
|
-
return <div>{user.name}</div>;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ✅ Good: per-request deduplication with React.cache
|
|
105
|
-
import { cache } from "react";
|
|
106
|
-
const getUser = cache(async () => {
|
|
107
|
-
return await db.user.findUnique({ where: { id: currentUserId } });
|
|
108
|
-
});
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### 4. Client Data Fetching (MEDIUM-HIGH)
|
|
112
|
-
|
|
113
|
-
| Rule | Description |
|
|
114
|
-
|------|-------------|
|
|
115
|
-
| `client-swr-dedup` | Auto-deduplicate requests with SWR |
|
|
116
|
-
| `client-event-listeners` | Prevent duplicate global event listener registration |
|
|
117
|
-
|
|
118
|
-
**Key Example — `client-swr-dedup` (MEDIUM-HIGH):**
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
// ❌ Bad: duplicate API calls from multiple components
|
|
122
|
-
function Header() {
|
|
123
|
-
const [user, setUser] = useState(null);
|
|
124
|
-
useEffect(() => { fetch("/api/user").then(r => r.json()).then(setUser); }, []);
|
|
125
|
-
return <div>{user?.name}</div>;
|
|
126
|
-
}
|
|
127
|
-
function Sidebar() {
|
|
128
|
-
const [user, setUser] = useState(null);
|
|
129
|
-
useEffect(() => { fetch("/api/user").then(r => r.json()).then(setUser); }, []);
|
|
130
|
-
return <div>{user?.email}</div>;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ✅ Good: auto-deduplication + caching with SWR
|
|
134
|
-
import useSWR from "swr";
|
|
135
|
-
function useUser() {
|
|
136
|
-
return useSWR("/api/user", fetcher);
|
|
137
|
-
}
|
|
138
|
-
function Header() {
|
|
139
|
-
const { data: user } = useUser(); // same key → auto-deduplicated
|
|
140
|
-
return <div>{user?.name}</div>;
|
|
141
|
-
}
|
|
142
|
-
function Sidebar() {
|
|
143
|
-
const { data: user } = useUser(); // only 1 network request
|
|
144
|
-
return <div>{user?.email}</div>;
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### 5. Re-render Optimization (MEDIUM)
|
|
149
|
-
|
|
150
|
-
| Rule | Description |
|
|
151
|
-
|------|-------------|
|
|
152
|
-
| `rerender-defer-reads` | Avoid subscribing to state used only in callbacks |
|
|
153
|
-
| `rerender-memo` | Isolate expensive computations in memoized components |
|
|
154
|
-
| `rerender-dependencies` | Use primitive values for effect dependencies |
|
|
155
|
-
| `rerender-derived-state` | Subscribe to derived booleans instead of raw data |
|
|
156
|
-
| `rerender-functional-setstate` | Use functional setState for stable callbacks |
|
|
157
|
-
| `rerender-lazy-state-init` | Pass functions for expensive initial values |
|
|
158
|
-
| `rerender-transitions` | Use startTransition for non-urgent updates |
|
|
159
|
-
|
|
160
|
-
**Key Example — `rerender-memo` (MEDIUM):**
|
|
161
|
-
|
|
162
|
-
```typescript
|
|
163
|
-
// ❌ Bad: expensive computation re-runs on every parent re-render
|
|
164
|
-
function Dashboard({ data, filter }) {
|
|
165
|
-
const processed = expensiveProcess(data); // runs every time
|
|
166
|
-
return <Chart data={processed} />;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// ✅ Good: isolate expensive computation in memoized component
|
|
170
|
-
const ProcessedChart = memo(function ProcessedChart({ data }: { data: Data[] }) {
|
|
171
|
-
const processed = expensiveProcess(data);
|
|
172
|
-
return <Chart data={processed} />;
|
|
173
|
-
});
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
### 6. Rendering Performance (MEDIUM)
|
|
177
|
-
|
|
178
|
-
| Rule | Description |
|
|
179
|
-
|------|-------------|
|
|
180
|
-
| `rendering-animate-svg-wrapper` | Animate div wrapper instead of SVG elements |
|
|
181
|
-
| `rendering-content-visibility` | Apply content-visibility to long lists |
|
|
182
|
-
| `rendering-hoist-jsx` | Hoist static JSX outside components |
|
|
183
|
-
| `rendering-svg-precision` | Reduce SVG coordinate precision |
|
|
184
|
-
| `rendering-hydration-no-flicker` | Prevent client-only data flicker with inline scripts |
|
|
185
|
-
| `rendering-activity` | Use Activity component for show/hide |
|
|
186
|
-
| `rendering-conditional-render` | Use ternary instead of `&&` |
|
|
187
|
-
|
|
188
|
-
**Key Example — `rendering-content-visibility` (MEDIUM):**
|
|
189
|
-
|
|
190
|
-
```css
|
|
191
|
-
/* ❌ Bad: render all items in a long list immediately */
|
|
192
|
-
.list-item {
|
|
193
|
-
/* default: render all items */
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/* ✅ Good: defer rendering for off-viewport items */
|
|
197
|
-
.list-item {
|
|
198
|
-
content-visibility: auto;
|
|
199
|
-
contain-intrinsic-size: 0 80px;
|
|
200
|
-
}
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
**Key Example — `rendering-hoist-jsx` (MEDIUM):**
|
|
204
|
-
|
|
205
|
-
```typescript
|
|
206
|
-
// ❌ Bad: static JSX recreated on every re-render
|
|
207
|
-
function Page({ data }) {
|
|
208
|
-
return (
|
|
209
|
-
<div>
|
|
210
|
-
<header><h1>My App</h1><nav>...</nav></header>
|
|
211
|
-
<main>{data.map(item => <Card key={item.id} {...item} />)}</main>
|
|
212
|
-
</div>
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ✅ Good: hoist static JSX outside component
|
|
217
|
-
const HEADER = <header><h1>My App</h1><nav>...</nav></header>;
|
|
218
|
-
|
|
219
|
-
function Page({ data }) {
|
|
220
|
-
return (
|
|
221
|
-
<div>
|
|
222
|
-
{HEADER}
|
|
223
|
-
<main>{data.map(item => <Card key={item.id} {...item} />)}</main>
|
|
224
|
-
</div>
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
### 7. JavaScript Performance (LOW-MEDIUM)
|
|
230
|
-
|
|
231
|
-
| Rule | Description |
|
|
232
|
-
|------|-------------|
|
|
233
|
-
| `js-batch-dom-css` | Batch CSS changes via class or cssText |
|
|
234
|
-
| `js-index-maps` | Index repeated lookups with Map |
|
|
235
|
-
| `js-cache-property-access` | Cache object property access in loops |
|
|
236
|
-
| `js-cache-function-results` | Cache function results in module-level Map |
|
|
237
|
-
| `js-cache-storage` | Cache localStorage/sessionStorage reads |
|
|
238
|
-
| `js-combine-iterations` | Combine multiple filter/map into single loop |
|
|
239
|
-
| `js-length-check-first` | Check array length before expensive comparisons |
|
|
240
|
-
| `js-early-exit` | Early return from functions |
|
|
241
|
-
| `js-hoist-regexp` | Hoist RegExp creation out of loops |
|
|
242
|
-
| `js-min-max-loop` | Calculate min/max with loop instead of sort |
|
|
243
|
-
| `js-set-map-lookups` | Use Set/Map for O(1) lookups |
|
|
244
|
-
| `js-tosorted-immutable` | Use toSorted() for immutable sorting |
|
|
245
|
-
|
|
246
|
-
### 8. Advanced Patterns (LOW)
|
|
247
|
-
|
|
248
|
-
| Rule | Description |
|
|
249
|
-
|------|-------------|
|
|
250
|
-
| `advanced-event-handler-refs` | Store event handlers in refs |
|
|
251
|
-
| `advanced-use-latest` | Use useLatest for stable callback refs |
|
|
252
|
-
|
|
253
|
-
**Key Example — `advanced-event-handler-refs` (LOW):**
|
|
254
|
-
|
|
255
|
-
```typescript
|
|
256
|
-
// ❌ Bad: event handler changes every render → effect re-runs
|
|
257
|
-
function useInterval(callback: () => void, delay: number) {
|
|
258
|
-
useEffect(() => {
|
|
259
|
-
const id = setInterval(callback, delay);
|
|
260
|
-
return () => clearInterval(id);
|
|
261
|
-
}, [callback, delay]); // callback changes every time
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// ✅ Good: ref holds latest handler → stable effect
|
|
265
|
-
function useInterval(callback: () => void, delay: number) {
|
|
266
|
-
const savedCallback = useRef(callback);
|
|
267
|
-
useEffect(() => { savedCallback.current = callback; });
|
|
268
|
-
useEffect(() => {
|
|
269
|
-
const id = setInterval(() => savedCallback.current(), delay);
|
|
270
|
-
return () => clearInterval(id);
|
|
271
|
-
}, [delay]); // only depends on delay
|
|
272
|
-
}
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
## Rule Count Summary by Category
|
|
276
|
-
|
|
277
|
-
| Category | Rules | Impact |
|
|
278
|
-
|----------|-------|--------|
|
|
279
|
-
| Waterfall Elimination | 5 | CRITICAL |
|
|
280
|
-
| Bundle Size Optimization | 5 | CRITICAL |
|
|
281
|
-
| Server-side Performance | 5 | HIGH |
|
|
282
|
-
| Client Data Fetching | 2 | MEDIUM-HIGH |
|
|
283
|
-
| Re-render Optimization | 7 | MEDIUM |
|
|
284
|
-
| Rendering Performance | 7 | MEDIUM |
|
|
285
|
-
| JavaScript Performance | 12 | LOW-MEDIUM |
|
|
286
|
-
| Advanced Patterns | 2 | LOW |
|
|
287
|
-
| **Total** | **45** | |
|
|
288
|
-
|
|
289
|
-
## Usage
|
|
290
|
-
|
|
291
|
-
This skill provides a Quick Reference index and key examples per category. For detailed explanations and full code examples of each rule, refer to the CCPP AGENTS.md.
|
|
292
|
-
|
|
293
|
-
### Priority-Based Application Strategy
|
|
294
|
-
|
|
295
|
-
1. **CRITICAL (Priority 1-2)**: Must apply in all projects
|
|
296
|
-
2. **HIGH (Priority 3)**: Apply when using server components
|
|
297
|
-
3. **MEDIUM (Priority 4-6)**: Review first when performance issues arise
|
|
298
|
-
4. **LOW (Priority 7-8)**: Selectively apply during optimization phase
|
|
299
|
-
|
|
300
|
-
### VIBE Tool Integration
|
|
301
|
-
|
|
302
|
-
- `core_analyze_complexity` — Analyze component complexity
|
|
303
|
-
- `core_validate_code_quality` — Validate code quality
|
|
304
|
-
- `/vibe.review` — 13+ agent parallel review (includes performance-reviewer)
|
|
55
|
+
- [ ] No sequential awaits for independent operations
|
|
56
|
+
- [ ] No barrel imports for tree-shakeable modules
|
|
57
|
+
- [ ] Server component data is `React.cache()`-wrapped where reused
|
|
58
|
+
- [ ] Heavy third-party loaded after hydration
|
|
59
|
+
- [ ] Long lists use `content-visibility: auto`
|
|
@@ -1,222 +1,51 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: video-production
|
|
3
|
-
description: "Video processing
|
|
3
|
+
description: "Video processing gotchas — FFmpeg, transcoding, streaming, subtitles."
|
|
4
4
|
triggers: [video, ffmpeg, transcode, encode, stream, media, subtitle, thumbnail, hls, dash]
|
|
5
5
|
priority: 60
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# Video Production
|
|
8
|
+
# Video Production
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## Pre-check (K1)
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
> Are you processing video files programmatically? If just embedding a YouTube/Vimeo player, this skill is not needed.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
## Gotchas
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
| Gotcha | Consequence | Prevention |
|
|
17
|
+
|--------|-------------|------------|
|
|
18
|
+
| Direct CLI string concatenation | Command injection risk | Always use wrapper library (fluent-ffmpeg for TS, ffmpeg-python for Python) |
|
|
19
|
+
| No input validation | Crash on corrupted files | Always `ffprobe` input before processing — check codec, resolution, duration |
|
|
20
|
+
| No temp file cleanup | Disk fills up silently | `try/finally` or cleanup handler — never leave partial outputs |
|
|
21
|
+
| No progress callback | Long encoding appears frozen | Implement progress events for any operation >10s |
|
|
22
|
+
| Memory loading large files | OOM on 4K+ video | Use streaming I/O, never read entire file into memory |
|
|
23
|
+
| Assuming codec availability | Fails on different FFmpeg builds | Check `ffmpeg -codecs` at runtime before encoding |
|
|
24
|
+
| Fixed bitrate encoding | Inconsistent quality | Use CRF-based quality (18-28 for H.264) instead |
|
|
25
|
+
| No timeout | Encoding hangs forever | Set timeout + kill process on expiry |
|
|
17
26
|
|
|
18
|
-
|
|
19
|
-
```typescript
|
|
20
|
-
import ffmpeg from 'fluent-ffmpeg';
|
|
27
|
+
## Codec Quick Reference
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
ffmpeg(input)
|
|
29
|
-
.outputOptions(buildOutputOptions(options))
|
|
30
|
-
.output(output)
|
|
31
|
-
.on('end', resolve)
|
|
32
|
-
.on('error', reject)
|
|
33
|
-
.run();
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
```
|
|
29
|
+
| Use Case | Codec | Note |
|
|
30
|
+
|----------|-------|------|
|
|
31
|
+
| Maximum compatibility | H.264 (libx264) | CRF 23 default |
|
|
32
|
+
| Smaller files | H.265 (libx265) | 50% smaller, slower, licensing issues |
|
|
33
|
+
| Open source | VP9 (libvpx-vp9) | Good for WebM |
|
|
34
|
+
| Best compression | AV1 (libaom-av1) | Very slow encoding |
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
```python
|
|
40
|
-
import ffmpeg
|
|
36
|
+
## Resolution Presets
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
(
|
|
49
|
-
ffmpeg
|
|
50
|
-
.input(input_path)
|
|
51
|
-
.output(output_path, vcodec=codec, crf=crf)
|
|
52
|
-
.overwrite_output()
|
|
53
|
-
.run(capture_stdout=True, capture_stderr=True)
|
|
54
|
-
)
|
|
55
|
-
```
|
|
38
|
+
| Preset | Resolution | Bitrate (H.264) |
|
|
39
|
+
|--------|-----------|-----------------|
|
|
40
|
+
| 360p | 640x360 | 800 kbps |
|
|
41
|
+
| 720p | 1280x720 | 3 Mbps |
|
|
42
|
+
| 1080p | 1920x1080 | 6 Mbps |
|
|
43
|
+
| 4K | 3840x2160 | 15 Mbps |
|
|
56
44
|
|
|
57
|
-
|
|
45
|
+
## Done Criteria (K4)
|
|
58
46
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
| Out of memory | 큰 파일 + 고해상도 | 스트리밍 처리, chunk 분할 |
|
|
65
|
-
| Corrupted input | 깨진 소스 파일 | `ffprobe`로 사전 검증 |
|
|
66
|
-
| Permission denied | 파일 잠금 | 임시 디렉토리 사용, 정리 보장 |
|
|
67
|
-
| Timeout | 장시간 인코딩 | progress 콜백 + 타임아웃 설정 |
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
// 입력 파일 사전 검증
|
|
71
|
-
async function probeVideo(filePath: string): Promise<VideoMetadata> {
|
|
72
|
-
return new Promise((resolve, reject) => {
|
|
73
|
-
ffmpeg.ffprobe(filePath, (err, data) => {
|
|
74
|
-
if (err) return reject(new Error(`Invalid video: ${err.message}`));
|
|
75
|
-
const video = data.streams.find(s => s.codec_type === 'video');
|
|
76
|
-
if (!video) return reject(new Error('No video stream found'));
|
|
77
|
-
resolve({
|
|
78
|
-
width: video.width ?? 0,
|
|
79
|
-
height: video.height ?? 0,
|
|
80
|
-
duration: Number(data.format.duration ?? 0),
|
|
81
|
-
codec: video.codec_name ?? 'unknown',
|
|
82
|
-
bitrate: Number(data.format.bit_rate ?? 0),
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Common Operations
|
|
90
|
-
|
|
91
|
-
### Thumbnail Generation
|
|
92
|
-
|
|
93
|
-
```typescript
|
|
94
|
-
function extractThumbnail(
|
|
95
|
-
input: string,
|
|
96
|
-
output: string,
|
|
97
|
-
timeSeconds: number = 1,
|
|
98
|
-
): Promise<void> {
|
|
99
|
-
return new Promise((resolve, reject) => {
|
|
100
|
-
ffmpeg(input)
|
|
101
|
-
.screenshots({
|
|
102
|
-
timestamps: [timeSeconds],
|
|
103
|
-
filename: path.basename(output),
|
|
104
|
-
folder: path.dirname(output),
|
|
105
|
-
size: '320x240',
|
|
106
|
-
})
|
|
107
|
-
.on('end', resolve)
|
|
108
|
-
.on('error', reject);
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
### Resolution Presets
|
|
114
|
-
|
|
115
|
-
| Preset | Resolution | Bitrate (H.264) | Use Case |
|
|
116
|
-
|--------|-----------|-----------------|----------|
|
|
117
|
-
| 360p | 640x360 | 800 kbps | Mobile preview |
|
|
118
|
-
| 480p | 854x480 | 1.5 Mbps | Standard mobile |
|
|
119
|
-
| 720p | 1280x720 | 3 Mbps | HD streaming |
|
|
120
|
-
| 1080p | 1920x1080 | 6 Mbps | Full HD |
|
|
121
|
-
| 4K | 3840x2160 | 15 Mbps | Ultra HD |
|
|
122
|
-
|
|
123
|
-
### Codec Selection
|
|
124
|
-
|
|
125
|
-
| Codec | Format | Pros | Cons |
|
|
126
|
-
|-------|--------|------|------|
|
|
127
|
-
| H.264 (libx264) | MP4 | Universal compatibility | Larger file size |
|
|
128
|
-
| H.265 (libx265) | MP4 | 50% smaller than H.264 | Slower encoding, licensing |
|
|
129
|
-
| VP9 (libvpx-vp9) | WebM | Open source, good quality | Slow encoding |
|
|
130
|
-
| AV1 (libaom-av1) | MP4/WebM | Best compression | Very slow encoding |
|
|
131
|
-
|
|
132
|
-
### HLS Streaming
|
|
133
|
-
|
|
134
|
-
```typescript
|
|
135
|
-
function generateHLS(
|
|
136
|
-
input: string,
|
|
137
|
-
outputDir: string,
|
|
138
|
-
variants: HLSVariant[] = DEFAULT_VARIANTS,
|
|
139
|
-
): Promise<void> {
|
|
140
|
-
return new Promise((resolve, reject) => {
|
|
141
|
-
const cmd = ffmpeg(input);
|
|
142
|
-
for (const v of variants) {
|
|
143
|
-
cmd
|
|
144
|
-
.output(path.join(outputDir, `${v.name}.m3u8`))
|
|
145
|
-
.outputOptions([
|
|
146
|
-
`-vf scale=${v.width}:${v.height}`,
|
|
147
|
-
`-b:v ${v.bitrate}`,
|
|
148
|
-
'-hls_time 6',
|
|
149
|
-
'-hls_list_size 0',
|
|
150
|
-
'-hls_segment_filename',
|
|
151
|
-
path.join(outputDir, `${v.name}_%03d.ts`),
|
|
152
|
-
]);
|
|
153
|
-
}
|
|
154
|
-
cmd.on('end', resolve).on('error', reject).run();
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### Subtitle Processing
|
|
160
|
-
|
|
161
|
-
```typescript
|
|
162
|
-
// SRT → VTT 변환
|
|
163
|
-
function srtToVtt(srtContent: string): string {
|
|
164
|
-
const vtt = srtContent
|
|
165
|
-
.replace(/\r\n/g, '\n')
|
|
166
|
-
.replace(/(\d{2}):(\d{2}):(\d{2}),(\d{3})/g, '$1:$2:$3.$4');
|
|
167
|
-
return `WEBVTT\n\n${vtt}`;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// 자막 burn-in (하드코딩)
|
|
171
|
-
function burnSubtitles(
|
|
172
|
-
input: string,
|
|
173
|
-
subtitleFile: string,
|
|
174
|
-
output: string,
|
|
175
|
-
): Promise<void> {
|
|
176
|
-
return new Promise((resolve, reject) => {
|
|
177
|
-
ffmpeg(input)
|
|
178
|
-
.outputOptions([`-vf subtitles=${subtitleFile}`])
|
|
179
|
-
.output(output)
|
|
180
|
-
.on('end', resolve)
|
|
181
|
-
.on('error', reject)
|
|
182
|
-
.run();
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### Watermark
|
|
188
|
-
|
|
189
|
-
```typescript
|
|
190
|
-
function addWatermark(
|
|
191
|
-
input: string,
|
|
192
|
-
watermark: string,
|
|
193
|
-
output: string,
|
|
194
|
-
position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' = 'bottom-right',
|
|
195
|
-
): Promise<void> {
|
|
196
|
-
const overlayMap: Record<string, string> = {
|
|
197
|
-
'top-left': '10:10',
|
|
198
|
-
'top-right': 'W-w-10:10',
|
|
199
|
-
'bottom-left': '10:H-h-10',
|
|
200
|
-
'bottom-right': 'W-w-10:H-h-10',
|
|
201
|
-
};
|
|
202
|
-
return new Promise((resolve, reject) => {
|
|
203
|
-
ffmpeg(input)
|
|
204
|
-
.input(watermark)
|
|
205
|
-
.complexFilter([`overlay=${overlayMap[position]}`])
|
|
206
|
-
.output(output)
|
|
207
|
-
.on('end', resolve)
|
|
208
|
-
.on('error', reject)
|
|
209
|
-
.run();
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
## Best Practices
|
|
215
|
-
|
|
216
|
-
1. **항상 ffprobe로 입력 검증** — 처리 전에 코덱, 해상도, 무결성 확인
|
|
217
|
-
2. **임시 파일 정리 보장** — try/finally 또는 cleanup handler 사용
|
|
218
|
-
3. **Progress 콜백 구현** — 장시간 작업에 진행률 피드백
|
|
219
|
-
4. **스트리밍 처리 선호** — 대용량 파일은 메모리에 올리지 않음
|
|
220
|
-
5. **코덱 가용성 런타임 확인** — `ffmpeg -codecs`로 빌드 지원 확인
|
|
221
|
-
6. **CRF 기반 품질 제어** — 비트레이트 고정보다 CRF (18-28) 사용
|
|
222
|
-
7. **하드웨어 가속 활용** — NVIDIA NVENC, Intel QSV, Apple VideoToolbox 가용 시 사용
|
|
47
|
+
- [ ] All FFmpeg calls go through wrapper library (no raw CLI strings)
|
|
48
|
+
- [ ] Input files validated with ffprobe before processing
|
|
49
|
+
- [ ] Temp files cleaned up in all paths (success + error)
|
|
50
|
+
- [ ] Progress reporting for long operations
|
|
51
|
+
- [ ] Codec availability checked at runtime
|