@kolbo/kolbo-code-linux-arm64-musl 1.1.72 → 1.1.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kolbo +0 -0
- package/package.json +1 -1
- package/skills/color-grading/SKILL.md +152 -0
- package/skills/ffmpeg-patterns/SKILL.md +240 -0
- package/skills/image-prompting-guide/SKILL.md +143 -0
- package/skills/kolbo/SKILL.md +29 -0
- package/skills/music-prompting/SKILL.md +146 -0
- package/skills/production-review/SKILL.md +152 -0
- package/skills/short-form-video/SKILL.md +168 -0
- package/skills/sound-design/SKILL.md +154 -0
- package/skills/storytelling/SKILL.md +139 -0
- package/skills/subtitle-production/SKILL.md +244 -0
- package/skills/subtitle-production/reference/burn_to_video.py +222 -0
- package/skills/subtitle-production/reference/export_srts.py +127 -0
- package/skills/subtitle-production/reference/gen_srt.py +42 -0
- package/skills/typography-video/SKILL.md +182 -0
- package/skills/typography-video/reference/KineticTitleScene.tsx +345 -0
- package/skills/video-editing/SKILL.md +128 -0
- package/skills/video-production/SKILL.md +7 -8
- package/skills/video-prompting-guide/SKILL.md +268 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KineticTitleScene — Full-screen kinetic text card
|
|
3
|
+
*
|
|
4
|
+
* bgStyle="solid" — always-on dark background (default)
|
|
5
|
+
* bgStyle="dynamic_panel" — bg wipes in before text, wipes out after text gone
|
|
6
|
+
* bgStyle="transparent" — no background (composite over footage, render with alpha codec)
|
|
7
|
+
*
|
|
8
|
+
* ALL words land simultaneously (staggered by 4 frames each).
|
|
9
|
+
* Alternating solid / outline / accent treatment.
|
|
10
|
+
* Based on Higgsfield / SOUL 2.0 reference style.
|
|
11
|
+
*/
|
|
12
|
+
import React from "react";
|
|
13
|
+
import { useCurrentFrame, useVideoConfig, spring, interpolate, Easing } from "remotion";
|
|
14
|
+
import { loadFont as loadPoppins } from "@remotion/google-fonts/Poppins";
|
|
15
|
+
import { loadFont as loadHeebo } from "@remotion/google-fonts/Heebo";
|
|
16
|
+
|
|
17
|
+
const { fontFamily: poppins } = loadPoppins();
|
|
18
|
+
const { fontFamily: heebo } = loadHeebo();
|
|
19
|
+
|
|
20
|
+
export interface KineticTitleProps {
|
|
21
|
+
words: string[]; // 2–4 words, each on its own line
|
|
22
|
+
subtext?: string; // optional small line below
|
|
23
|
+
accentColor?: string; // default brand blue
|
|
24
|
+
language?: string; // "en" | "he"
|
|
25
|
+
bgStyle?: "solid" | "dynamic_panel" | "transparent";
|
|
26
|
+
durationInFrames: number;
|
|
27
|
+
fps: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const BRAND_BLUE = "#60a5fa";
|
|
31
|
+
const STAGGER = 4; // frames between each word entrance
|
|
32
|
+
|
|
33
|
+
// How many frames the bg leads the text on entry, and trails it on exit
|
|
34
|
+
const BG_LEAD = 10; // bg starts wiping in 10 frames before text
|
|
35
|
+
const BG_TRAIL = 12; // bg stays for 12 frames after text is gone
|
|
36
|
+
|
|
37
|
+
export const KineticTitleScene: React.FC<KineticTitleProps> = ({
|
|
38
|
+
words = ["WENT", "VIRAL"],
|
|
39
|
+
subtext,
|
|
40
|
+
accentColor = BRAND_BLUE,
|
|
41
|
+
language = "en",
|
|
42
|
+
bgStyle = "solid",
|
|
43
|
+
durationInFrames = 150,
|
|
44
|
+
fps = 30,
|
|
45
|
+
}) => {
|
|
46
|
+
const frame = useCurrentFrame();
|
|
47
|
+
const { width, height } = useVideoConfig();
|
|
48
|
+
|
|
49
|
+
const isHebrew = language === "he" || language === "iw";
|
|
50
|
+
const fontFamily = isHebrew ? heebo : poppins;
|
|
51
|
+
const isVertical = height > width;
|
|
52
|
+
const accent = accentColor;
|
|
53
|
+
const isDynamic = bgStyle === "dynamic_panel";
|
|
54
|
+
const isTransparent = bgStyle === "transparent";
|
|
55
|
+
|
|
56
|
+
// ── Text timing ──────────────────────────────────────────────────────────
|
|
57
|
+
// Dynamic: text starts after bg has partially appeared
|
|
58
|
+
const textOffset = isDynamic ? BG_LEAD : 0;
|
|
59
|
+
const textExitStart = durationInFrames - (isDynamic ? BG_TRAIL + 12 : 12);
|
|
60
|
+
|
|
61
|
+
const exitOpacity = interpolate(
|
|
62
|
+
frame,
|
|
63
|
+
[textExitStart, textExitStart + 12],
|
|
64
|
+
[1, 0],
|
|
65
|
+
{ extrapolateRight: "clamp" }
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Global entrance scale
|
|
69
|
+
const globalScale = interpolate(frame - textOffset, [0, 20], [1.04, 1.0], {
|
|
70
|
+
extrapolateLeft: "clamp",
|
|
71
|
+
extrapolateRight: "clamp",
|
|
72
|
+
easing: Easing.out(Easing.cubic),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ── Background panel timing ───────────────────────────────────────────────
|
|
76
|
+
// BG enters: wipes left→right starting at frame 0
|
|
77
|
+
const bgEnterDuration = 18;
|
|
78
|
+
const bgEnterProgress = interpolate(frame, [0, bgEnterDuration], [0, 1], {
|
|
79
|
+
extrapolateLeft: "clamp",
|
|
80
|
+
extrapolateRight: "clamp",
|
|
81
|
+
easing: Easing.out(Easing.cubic),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// BG exits: wipes left→right starting after text is fully gone
|
|
85
|
+
const bgExitStart = textExitStart + 12;
|
|
86
|
+
const bgExitProgress = interpolate(
|
|
87
|
+
frame,
|
|
88
|
+
[bgExitStart, bgExitStart + 14],
|
|
89
|
+
[0, 1],
|
|
90
|
+
{
|
|
91
|
+
extrapolateLeft: "clamp",
|
|
92
|
+
extrapolateRight: "clamp",
|
|
93
|
+
easing: Easing.in(Easing.cubic),
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// scaleX wipe — most reliable in Remotion headless Chrome
|
|
98
|
+
// Enter: scaleX grows 0→1 from LEFT origin (reveals left to right)
|
|
99
|
+
// Exit: scaleX shrinks 1→0 from RIGHT origin (removes left to right)
|
|
100
|
+
const bgScaleX = isDynamic
|
|
101
|
+
? (frame < bgExitStart
|
|
102
|
+
? interpolate(bgEnterProgress, [0, 1], [0, 1])
|
|
103
|
+
: interpolate(bgExitProgress, [0, 1], [1, 0]))
|
|
104
|
+
: 1;
|
|
105
|
+
const bgTransformOrigin = isDynamic && frame >= bgExitStart ? "right center" : "left center";
|
|
106
|
+
|
|
107
|
+
const showBg = !isTransparent;
|
|
108
|
+
|
|
109
|
+
// ── Accent scan line across bg (cinematic HUD feel) ──────────────────────
|
|
110
|
+
const scanY = interpolate(frame, [0, durationInFrames], [0, height], {
|
|
111
|
+
extrapolateRight: "clamp",
|
|
112
|
+
});
|
|
113
|
+
const scanOpacity = isDynamic
|
|
114
|
+
? bgEnterProgress * (1 - bgExitProgress) * 0.12
|
|
115
|
+
: 0.06;
|
|
116
|
+
|
|
117
|
+
// ── Font size ─────────────────────────────────────────────────────────────
|
|
118
|
+
const baseFontSize = isVertical
|
|
119
|
+
? Math.round(width * 0.22)
|
|
120
|
+
: Math.round(height * 0.26);
|
|
121
|
+
|
|
122
|
+
// ── Word styles: solid → outline → accent ────────────────────────────────
|
|
123
|
+
const wordStyles = (i: number) => {
|
|
124
|
+
const mod = i % 3;
|
|
125
|
+
if (mod === 0) return { color: "#ffffff", stroke: "none", shadow: `0 0 40px ${accent}44` };
|
|
126
|
+
if (mod === 1) return { color: "transparent", stroke: accent, shadow: `0 0 30px ${accent}44` };
|
|
127
|
+
return { color: accent, stroke: "none", shadow: `0 0 50px ${accent}88` };
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// ── Separator line ────────────────────────────────────────────────────────
|
|
131
|
+
const SepLine = ({ delay }: { delay: number }) => {
|
|
132
|
+
const lineW = interpolate(frame - delay, [0, 16], [0, 1], {
|
|
133
|
+
extrapolateRight: "clamp",
|
|
134
|
+
easing: Easing.out(Easing.cubic),
|
|
135
|
+
});
|
|
136
|
+
return (
|
|
137
|
+
<div style={{
|
|
138
|
+
width: `${lineW * 100}%`,
|
|
139
|
+
height: 1,
|
|
140
|
+
background: `linear-gradient(${isHebrew ? "270deg" : "90deg"}, transparent, ${accent}88, transparent)`,
|
|
141
|
+
marginBottom: 2,
|
|
142
|
+
marginTop: 2,
|
|
143
|
+
}} />
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div style={{
|
|
149
|
+
width, height,
|
|
150
|
+
// dynamic_panel: outer is transparent — the animated inner panel owns the background
|
|
151
|
+
background: (isTransparent || isDynamic) ? "transparent" : "#07070f",
|
|
152
|
+
overflow: "hidden",
|
|
153
|
+
position: "relative",
|
|
154
|
+
fontFamily,
|
|
155
|
+
direction: isHebrew ? "rtl" : "ltr",
|
|
156
|
+
}}>
|
|
157
|
+
|
|
158
|
+
{/* ── Background panel (with clip-path wipe) ── */}
|
|
159
|
+
{showBg && (
|
|
160
|
+
<div style={{
|
|
161
|
+
position: "absolute", inset: 0,
|
|
162
|
+
transform: isDynamic ? `scaleX(${bgScaleX})` : undefined,
|
|
163
|
+
transformOrigin: isDynamic ? bgTransformOrigin : undefined,
|
|
164
|
+
}}>
|
|
165
|
+
{/* Dark fill */}
|
|
166
|
+
<div style={{ position: "absolute", inset: 0, background: "#07070f" }} />
|
|
167
|
+
|
|
168
|
+
{/* Grid lines */}
|
|
169
|
+
<div style={{
|
|
170
|
+
position: "absolute", inset: 0,
|
|
171
|
+
backgroundImage: [
|
|
172
|
+
`linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px)`,
|
|
173
|
+
`linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px)`,
|
|
174
|
+
].join(", "),
|
|
175
|
+
backgroundSize: "60px 60px",
|
|
176
|
+
pointerEvents: "none",
|
|
177
|
+
}} />
|
|
178
|
+
|
|
179
|
+
{/* Diagonal corner accent */}
|
|
180
|
+
<div style={{
|
|
181
|
+
position: "absolute",
|
|
182
|
+
top: -60,
|
|
183
|
+
left: isHebrew ? undefined : -60,
|
|
184
|
+
right: isHebrew ? -60 : undefined,
|
|
185
|
+
width: 300, height: 300,
|
|
186
|
+
background: `linear-gradient(${isHebrew ? "225deg" : "135deg"}, ${accent}18 0%, transparent 60%)`,
|
|
187
|
+
pointerEvents: "none",
|
|
188
|
+
}} />
|
|
189
|
+
|
|
190
|
+
{/* Vignette */}
|
|
191
|
+
<div style={{
|
|
192
|
+
position: "absolute", inset: 0,
|
|
193
|
+
background: "radial-gradient(ellipse at center, transparent 30%, rgba(0,0,0,0.65) 100%)",
|
|
194
|
+
pointerEvents: "none",
|
|
195
|
+
}} />
|
|
196
|
+
|
|
197
|
+
{/* Horizontal scan line (slow drift) */}
|
|
198
|
+
<div style={{
|
|
199
|
+
position: "absolute",
|
|
200
|
+
left: 0, right: 0,
|
|
201
|
+
top: scanY,
|
|
202
|
+
height: 1,
|
|
203
|
+
background: `linear-gradient(90deg, transparent, ${accent}, transparent)`,
|
|
204
|
+
opacity: scanOpacity,
|
|
205
|
+
pointerEvents: "none",
|
|
206
|
+
}} />
|
|
207
|
+
|
|
208
|
+
{/* Bright edge line at wipe front — sits at the right edge of the scaled panel */}
|
|
209
|
+
{isDynamic && bgScaleX > 0 && bgScaleX < 1 && (
|
|
210
|
+
<div style={{
|
|
211
|
+
position: "absolute",
|
|
212
|
+
top: 0, bottom: 0,
|
|
213
|
+
right: 0,
|
|
214
|
+
width: Math.round(2 / Math.max(bgScaleX, 0.05)), // compensate for scaleX compression
|
|
215
|
+
background: `linear-gradient(180deg, transparent, ${accent}, ${accent}, transparent)`,
|
|
216
|
+
opacity: 0.9,
|
|
217
|
+
boxShadow: `0 0 16px ${accent}, 0 0 32px ${accent}88`,
|
|
218
|
+
}} />
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
|
|
223
|
+
{/* ── Word stack ── */}
|
|
224
|
+
<div style={{
|
|
225
|
+
position: "absolute",
|
|
226
|
+
inset: 0,
|
|
227
|
+
display: "flex",
|
|
228
|
+
flexDirection: "column",
|
|
229
|
+
justifyContent: "center",
|
|
230
|
+
paddingLeft: isHebrew ? 0 : isVertical ? 32 : 64,
|
|
231
|
+
paddingRight: isHebrew ? (isVertical ? 32 : 64) : 0,
|
|
232
|
+
paddingTop: isVertical ? 40 : 30,
|
|
233
|
+
transform: `scale(${globalScale})`,
|
|
234
|
+
opacity: exitOpacity,
|
|
235
|
+
}}>
|
|
236
|
+
{words.map((word, i) => {
|
|
237
|
+
const delay = textOffset + i * STAGGER;
|
|
238
|
+
const wordScale = spring({
|
|
239
|
+
frame: Math.max(0, frame - delay),
|
|
240
|
+
fps,
|
|
241
|
+
from: 0,
|
|
242
|
+
to: 1,
|
|
243
|
+
config: { damping: 9, stiffness: 350 },
|
|
244
|
+
});
|
|
245
|
+
const wordY = interpolate(
|
|
246
|
+
frame - delay, [0, 14], [40, 0],
|
|
247
|
+
{ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) }
|
|
248
|
+
);
|
|
249
|
+
const ws = wordStyles(i);
|
|
250
|
+
|
|
251
|
+
const glitchAmt = i === 0
|
|
252
|
+
? interpolate(frame - textOffset, [0, 1, 3], [12, 4, 0], { extrapolateRight: "clamp" })
|
|
253
|
+
: 0;
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<React.Fragment key={i}>
|
|
257
|
+
{i > 0 && <SepLine delay={textOffset + (i - 1) * STAGGER + 6} />}
|
|
258
|
+
<div style={{
|
|
259
|
+
position: "relative",
|
|
260
|
+
transform: `translateY(${wordY}px) scaleY(${wordScale})`,
|
|
261
|
+
transformOrigin: isHebrew ? "top right" : "top left",
|
|
262
|
+
lineHeight: 0.85,
|
|
263
|
+
overflow: "visible",
|
|
264
|
+
}}>
|
|
265
|
+
{glitchAmt > 0 && (
|
|
266
|
+
<div style={{
|
|
267
|
+
position: "absolute",
|
|
268
|
+
fontSize: baseFontSize,
|
|
269
|
+
fontWeight: 900,
|
|
270
|
+
color: accent,
|
|
271
|
+
textTransform: isHebrew ? "none" : "uppercase",
|
|
272
|
+
letterSpacing: isHebrew ? 0 : -2,
|
|
273
|
+
transform: `translateX(${glitchAmt}px)`,
|
|
274
|
+
opacity: 0.45,
|
|
275
|
+
mixBlendMode: "screen",
|
|
276
|
+
userSelect: "none",
|
|
277
|
+
}}>{word}</div>
|
|
278
|
+
)}
|
|
279
|
+
<div style={{
|
|
280
|
+
fontSize: baseFontSize,
|
|
281
|
+
fontWeight: 900,
|
|
282
|
+
color: ws.color,
|
|
283
|
+
textTransform: isHebrew ? "none" : "uppercase",
|
|
284
|
+
letterSpacing: isHebrew ? 0 : -2,
|
|
285
|
+
WebkitTextStroke: ws.stroke !== "none" ? `4px ${ws.stroke}` : undefined,
|
|
286
|
+
textShadow: ws.shadow !== "none" ? ws.shadow : undefined,
|
|
287
|
+
whiteSpace: "nowrap",
|
|
288
|
+
userSelect: "none",
|
|
289
|
+
}}>{word}</div>
|
|
290
|
+
</div>
|
|
291
|
+
</React.Fragment>
|
|
292
|
+
);
|
|
293
|
+
})}
|
|
294
|
+
|
|
295
|
+
{subtext && (() => {
|
|
296
|
+
const stDelay = textOffset + words.length * STAGGER + 10;
|
|
297
|
+
const stOpacity = interpolate(frame - stDelay, [0, 14], [0, 1], {
|
|
298
|
+
extrapolateLeft: "clamp", extrapolateRight: "clamp",
|
|
299
|
+
});
|
|
300
|
+
return (
|
|
301
|
+
<div style={{
|
|
302
|
+
fontSize: Math.round(baseFontSize * 0.17),
|
|
303
|
+
fontWeight: 600,
|
|
304
|
+
color: "rgba(255,255,255,0.45)",
|
|
305
|
+
textTransform: isHebrew ? "none" : "uppercase",
|
|
306
|
+
letterSpacing: isHebrew ? 0 : 5,
|
|
307
|
+
marginTop: 20,
|
|
308
|
+
opacity: stOpacity * exitOpacity,
|
|
309
|
+
}}>{subtext}</div>
|
|
310
|
+
);
|
|
311
|
+
})()}
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
{/* ── Viewfinder corners ── */}
|
|
315
|
+
{Math.floor(frame / 10) % 2 === 0 && [
|
|
316
|
+
{ top: 24, left: 24, borderTopWidth: 2, borderLeftWidth: 2 },
|
|
317
|
+
{ top: 24, right: 24, borderTopWidth: 2, borderRightWidth: 2 },
|
|
318
|
+
{ bottom: 24, left: 24, borderBottomWidth: 2, borderLeftWidth: 2 },
|
|
319
|
+
{ bottom: 24, right: 24, borderBottomWidth: 2, borderRightWidth: 2 },
|
|
320
|
+
].map((pos, i) => (
|
|
321
|
+
<div key={i} style={{
|
|
322
|
+
position: "absolute", width: 20, height: 20,
|
|
323
|
+
borderColor: accent, borderStyle: "solid", borderWidth: 0,
|
|
324
|
+
opacity: isTransparent ? exitOpacity * 0.5 : 0.5,
|
|
325
|
+
...pos,
|
|
326
|
+
}} />
|
|
327
|
+
))}
|
|
328
|
+
|
|
329
|
+
{/* ── Frame counter ── */}
|
|
330
|
+
<div style={{
|
|
331
|
+
position: "absolute",
|
|
332
|
+
bottom: isVertical ? 32 : 24,
|
|
333
|
+
right: isHebrew ? undefined : 36,
|
|
334
|
+
left: isHebrew ? 36 : undefined,
|
|
335
|
+
fontSize: 11,
|
|
336
|
+
fontFamily: "monospace",
|
|
337
|
+
color: `${accent}66`,
|
|
338
|
+
letterSpacing: 3,
|
|
339
|
+
opacity: exitOpacity,
|
|
340
|
+
}}>
|
|
341
|
+
{String(frame).padStart(4, "0")}
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
);
|
|
345
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: video-editing
|
|
3
|
+
description: >
|
|
4
|
+
Video editing decisions: what to cut vs keep, cut techniques (J-cut, L-cut, hard cut),
|
|
5
|
+
pacing by format, edit decision structure, silence/filler removal, talking head editing.
|
|
6
|
+
Use when making editorial decisions about video content.
|
|
7
|
+
Keywords: video editing, cut, trim, j-cut, l-cut, pacing, filler words, silence, talking head,
|
|
8
|
+
edit decision, transition, dead air, false start
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Video Editing — Editorial Decisions
|
|
12
|
+
|
|
13
|
+
## What to Cut
|
|
14
|
+
|
|
15
|
+
1. **Filler words:** "um", "uh", "like", "you know" — cut at word boundaries using word timestamps
|
|
16
|
+
2. **False starts:** When the speaker restarts a sentence, keep only the final take
|
|
17
|
+
3. **Dead air:** Silence longer than 1.5 seconds should be trimmed to ~0.5 seconds
|
|
18
|
+
4. **Off-topic tangents:** If the speaker wanders, cut to the next relevant segment
|
|
19
|
+
5. **Repeated points:** Keep the best delivery, remove redundant takes
|
|
20
|
+
|
|
21
|
+
## What NOT to Cut
|
|
22
|
+
|
|
23
|
+
- **Breath pauses:** Natural 0.3-0.8 second pauses between sentences. These sound natural.
|
|
24
|
+
- **Emphasis pauses:** Intentional pauses for dramatic effect
|
|
25
|
+
- **Reactions and transitions:** Verbal bridges like "So..." or "Now..." that provide flow
|
|
26
|
+
|
|
27
|
+
## Cut Techniques
|
|
28
|
+
|
|
29
|
+
| Technique | Description | When to Use |
|
|
30
|
+
|-----------|-------------|-------------|
|
|
31
|
+
| **J-cut** | Audio from next segment starts ~0.5s before visual cut | Smooth transitions |
|
|
32
|
+
| **L-cut** | Audio from current segment continues ~0.5s after visual cut | Maintaining continuity |
|
|
33
|
+
| **Hard cut** | Instant transition | Major topic changes |
|
|
34
|
+
| **Jump cut** | Cut within same shot (visible jump) | YouTube/social energy, pacing |
|
|
35
|
+
| **Match cut** | Visual similarity bridges two different shots | Creative storytelling |
|
|
36
|
+
|
|
37
|
+
## Pacing by Format
|
|
38
|
+
|
|
39
|
+
| Format | Approach |
|
|
40
|
+
|--------|----------|
|
|
41
|
+
| **Short-form (< 60s)** | Aggressive cuts. Minimal dead air. High energy. Visual change every 1-3s |
|
|
42
|
+
| **Medium-form (1-10 min)** | Balanced. Keep natural pauses for breathing room. Change every 3-5s |
|
|
43
|
+
| **Long-form (> 10 min)** | Let scenes breathe. Only cut obvious problems. Change every 5-10s |
|
|
44
|
+
|
|
45
|
+
## Edit Decision Structure
|
|
46
|
+
|
|
47
|
+
When planning an edit, define:
|
|
48
|
+
|
|
49
|
+
- **Cuts:** Ordered list of segments to keep (source, in/out points, speed)
|
|
50
|
+
- **Overlays:** Timed overlay placements (images, diagrams, lower thirds)
|
|
51
|
+
- **Subtitles:** Subtitle configuration (enabled, style, source file)
|
|
52
|
+
- **Music:** Background music settings (asset, volume, ducking, fades)
|
|
53
|
+
- **Transitions:** Transition type and timing between cuts
|
|
54
|
+
|
|
55
|
+
## Silence Removal Workflow
|
|
56
|
+
|
|
57
|
+
1. **Detect silence** with FFmpeg: `silencedetect=noise=-35dB:d=0.4`
|
|
58
|
+
2. **Parse** silence_start/silence_end timestamps from stderr
|
|
59
|
+
3. **Generate segments** of non-silent audio
|
|
60
|
+
4. **Concatenate** segments with the concat demuxer
|
|
61
|
+
5. Optional: apply `atempo=1.14` for subtle speedup that feels natural
|
|
62
|
+
|
|
63
|
+
## Talking Head Editing Checklist
|
|
64
|
+
|
|
65
|
+
- [ ] No visible jump cuts without intentional style choice
|
|
66
|
+
- [ ] Audio doesn't pop or click at cut points
|
|
67
|
+
- [ ] Pacing matches content energy and target platform
|
|
68
|
+
- [ ] Speaker's face is never covered by overlays
|
|
69
|
+
- [ ] All cuts are at word boundaries (not mid-word)
|
|
70
|
+
- [ ] Filler words removed unless they serve the personality
|
|
71
|
+
- [ ] B-roll covers any remaining jump cuts
|
|
72
|
+
|
|
73
|
+
## Lip Sync (Dubbing / Localization)
|
|
74
|
+
|
|
75
|
+
When replacing audio and matching lips:
|
|
76
|
+
|
|
77
|
+
| Input | Tool | Output |
|
|
78
|
+
|-------|------|--------|
|
|
79
|
+
| Existing VIDEO + new audio | Lip sync | Video with synced lips |
|
|
80
|
+
| Still PHOTO + audio | Talking head generator | New video from photo |
|
|
81
|
+
|
|
82
|
+
**Decision rule:** If you have video footage of the person, use lip sync. If you only have a photo, use talking head generation.
|
|
83
|
+
|
|
84
|
+
**Workflow for localization:**
|
|
85
|
+
```
|
|
86
|
+
transcribe(video) → translate → TTS(translated text) → lip_sync(original_video, new_audio)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Keep original video as source for each language — never chain lip sync outputs.
|
|
90
|
+
|
|
91
|
+
**Face padding** for lip sync: `[0, 10, 0, 0]` (top, bottom, left, right) works for 90% of footage. Increase bottom if chin gets cropped.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Kolbo MCP Integration
|
|
96
|
+
|
|
97
|
+
| Task | Kolbo MCP Tool | Notes |
|
|
98
|
+
|------|---------------|-------|
|
|
99
|
+
| Transcribe for edit points | `transcribe_audio` | Word-level timestamps for precise cuts |
|
|
100
|
+
| Lip sync dubbing | `generate_lipsync` | Source video + new audio |
|
|
101
|
+
| Generate B-roll | `generate_video` or `generate_image` | Cover jump cuts |
|
|
102
|
+
| Generate narration | `generate_speech` | Re-record with AI voice |
|
|
103
|
+
| Visual analysis | `chat_send_message` + Gemini | "Analyze this video for edit points" |
|
|
104
|
+
|
|
105
|
+
**Editing workflow with Kolbo:**
|
|
106
|
+
1. `transcribe_audio` → get full transcript with word timestamps
|
|
107
|
+
2. Identify filler words, dead air, false starts from transcript
|
|
108
|
+
3. Generate FFmpeg trim commands for non-silent/non-filler segments
|
|
109
|
+
4. `generate_image` or `generate_video` → B-roll for covering jump cuts
|
|
110
|
+
5. Concatenate clips + burn-in subtitles + mix audio
|
|
111
|
+
6. Review with `production-review` skill
|
|
112
|
+
|
|
113
|
+
**Localization workflow:**
|
|
114
|
+
1. `transcribe_audio` → source language transcript
|
|
115
|
+
2. Translate the transcript (use `chat_send_message` for translation)
|
|
116
|
+
3. `generate_speech` → TTS in target language
|
|
117
|
+
4. `generate_lipsync` → sync new audio to original face
|
|
118
|
+
5. Repeat for each language (always from original, never chain)
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Local / Free Options
|
|
123
|
+
|
|
124
|
+
> **IMPORTANT:** Always use Kolbo MCP tools by default (`transcribe_audio`, `generate_lipsync`). FFmpeg silence removal is safe to use directly. For anything else, confirm with the user first.
|
|
125
|
+
|
|
126
|
+
**FFmpeg (safe, standard):** Silence detection/removal, trimming, concatenation — all built-in.
|
|
127
|
+
|
|
128
|
+
**Transcription:** If the user wants offline, `faster-whisper` runs on CPU (`pip install faster-whisper`). Confirm before installing.
|
|
@@ -105,16 +105,15 @@ def detect_silence(video_path, noise_db=-35, duration=0.4):
|
|
|
105
105
|
|
|
106
106
|
## RTL (Hebrew/Arabic) Subtitles
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
For comprehensive RTL subtitle handling, load the `subtitle-production` skill — it contains full patterns for:
|
|
109
|
+
- Simple SRT burn-in with Heebo font + `Encoding=177`
|
|
110
|
+
- ASS per-word positioning for karaoke (with PIL `~0.74` scale factor)
|
|
111
|
+
- Remotion RTL captions with CSS `direction: rtl` and all the flip rules
|
|
112
|
+
- RTL progress bar with FFmpeg `geq` filter
|
|
109
113
|
|
|
110
|
-
|
|
111
|
-
- Use PIL to measure word widths: apply `~0.74` scale factor (PIL→libass calibration)
|
|
112
|
-
- Use `Alignment=7` (top-left anchor) so `\pos` sets exact top-left of each word
|
|
113
|
-
- Set `Encoding=177` (Hebrew) in ASS style
|
|
114
|
-
- Strip punctuation and render as separate positioned elements
|
|
115
|
-
- Two ASS styles (e.g., White + Yellow) instead of inline `\c` color tags
|
|
114
|
+
**CRITICAL**: Any inline ASS tag (`\c`, `\K`, `\1c`, etc.) between RTL words breaks Unicode bidi in libass — words render LTR. Use separate Dialogue lines per word instead.
|
|
116
115
|
|
|
117
|
-
|
|
116
|
+
For Remotion RTL layout rules (padding flips, transform-origin, gradient direction), load the `typography-video` skill.
|
|
118
117
|
|
|
119
118
|
## Remotion Motion Graphics
|
|
120
119
|
|