@biggora/claude-plugins 1.1.1 → 1.2.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/.claude/settings.local.json +3 -1
- package/README.md +24 -17
- package/package.json +1 -1
- package/registry/registry.json +319 -244
- package/specs/coding.md +24 -0
- package/specs/pod.md +2 -0
- package/src/skills/captcha/README.md +221 -0
- package/src/skills/captcha/SKILL.md +355 -0
- package/src/skills/captcha/references/captcha-types.md +254 -0
- package/src/skills/captcha/references/services.md +172 -0
- package/src/skills/captcha/references/stealth.md +238 -0
- package/src/skills/captcha/scripts/solve_captcha.py +323 -0
- package/src/skills/captcha/scripts/solve_image_grid.py +350 -0
- package/src/skills/codex-cli/SKILL.md +21 -11
- package/src/skills/gemini-cli/SKILL.md +27 -13
- package/src/skills/gemini-cli/references/commands.md +21 -14
- package/src/skills/gemini-cli/references/configuration.md +23 -18
- package/src/skills/gemini-cli/references/headless-and-scripting.md +7 -17
- package/src/skills/gemini-cli/references/mcp-and-extensions.md +12 -6
- package/src/skills/google-merchant-api/SKILL.md +581 -0
- package/src/skills/google-merchant-api/references/accounts.md +247 -0
- package/src/skills/google-merchant-api/references/content-api-legacy.md +216 -0
- package/src/skills/google-merchant-api/references/datasources.md +233 -0
- package/src/skills/google-merchant-api/references/inventories.md +201 -0
- package/src/skills/google-merchant-api/references/migration.md +267 -0
- package/src/skills/google-merchant-api/references/products.md +316 -0
- package/src/skills/google-merchant-api/references/promotions.md +201 -0
- package/src/skills/google-merchant-api/references/reports.md +240 -0
- package/src/skills/lv-aggregators-api/SKILL.md +113 -0
- package/src/skills/lv-aggregators-api/references/integration-guide.md +368 -0
- package/src/skills/lv-aggregators-api/references/kurpirkt.md +103 -0
- package/src/skills/lv-aggregators-api/references/salidzini.md +122 -0
- package/src/skills/notebook-lm/SKILL.md +1 -1
- package/src/skills/screen-recording/SKILL.md +243 -213
- package/src/skills/screen-recording/references/design-patterns.md +4 -2
- package/src/skills/screen-recording/references/ffmpeg-recording.md +473 -0
- package/src/skills/screen-recording/references/{approach1-programmatic.md → programmatic-generation.md} +45 -22
- package/src/skills/screen-recording/references/python-fallback.md +222 -0
- package/src/skills/tailwindcss-best-practices/SKILL.md +180 -0
- package/src/skills/tailwindcss-best-practices/references/best-practices-utility-patterns.md +87 -0
- package/src/skills/tailwindcss-best-practices/references/core-installation.md +109 -0
- package/src/skills/tailwindcss-best-practices/references/core-preflight.md +200 -0
- package/src/skills/tailwindcss-best-practices/references/core-responsive.md +163 -0
- package/src/skills/tailwindcss-best-practices/references/core-source-detection.md +114 -0
- package/src/skills/tailwindcss-best-practices/references/core-theme.md +108 -0
- package/src/skills/tailwindcss-best-practices/references/core-utility-classes.md +59 -0
- package/src/skills/tailwindcss-best-practices/references/core-variants.md +204 -0
- package/src/skills/tailwindcss-best-practices/references/effects-form-controls.md +76 -0
- package/src/skills/tailwindcss-best-practices/references/effects-mask.md +91 -0
- package/src/skills/tailwindcss-best-practices/references/effects-scroll-snap.md +59 -0
- package/src/skills/tailwindcss-best-practices/references/effects-text-shadow.md +78 -0
- package/src/skills/tailwindcss-best-practices/references/effects-transition-animation.md +80 -0
- package/src/skills/tailwindcss-best-practices/references/effects-visibility-interactivity.md +82 -0
- package/src/skills/tailwindcss-best-practices/references/features-content-detection.md +175 -0
- package/src/skills/tailwindcss-best-practices/references/features-custom-styles.md +203 -0
- package/src/skills/tailwindcss-best-practices/references/features-dark-mode.md +137 -0
- package/src/skills/tailwindcss-best-practices/references/features-functions-directives.md +241 -0
- package/src/skills/tailwindcss-best-practices/references/features-upgrade.md +160 -0
- package/src/skills/tailwindcss-best-practices/references/layout-aspect-ratio.md +39 -0
- package/src/skills/tailwindcss-best-practices/references/layout-columns.md +80 -0
- package/src/skills/tailwindcss-best-practices/references/layout-display.md +110 -0
- package/src/skills/tailwindcss-best-practices/references/layout-flexbox.md +112 -0
- package/src/skills/tailwindcss-best-practices/references/layout-grid.md +87 -0
- package/src/skills/tailwindcss-best-practices/references/layout-height.md +97 -0
- package/src/skills/tailwindcss-best-practices/references/layout-inset.md +103 -0
- package/src/skills/tailwindcss-best-practices/references/layout-logical-properties.md +92 -0
- package/src/skills/tailwindcss-best-practices/references/layout-margin.md +126 -0
- package/src/skills/tailwindcss-best-practices/references/layout-min-max-sizing.md +63 -0
- package/src/skills/tailwindcss-best-practices/references/layout-object-fit-position.md +64 -0
- package/src/skills/tailwindcss-best-practices/references/layout-overflow.md +57 -0
- package/src/skills/tailwindcss-best-practices/references/layout-padding.md +77 -0
- package/src/skills/tailwindcss-best-practices/references/layout-position.md +85 -0
- package/src/skills/tailwindcss-best-practices/references/layout-tables.md +67 -0
- package/src/skills/tailwindcss-best-practices/references/layout-width.md +102 -0
- package/src/skills/tailwindcss-best-practices/references/transform-base.md +68 -0
- package/src/skills/tailwindcss-best-practices/references/transform-rotate.md +70 -0
- package/src/skills/tailwindcss-best-practices/references/transform-scale.md +83 -0
- package/src/skills/tailwindcss-best-practices/references/transform-skew.md +62 -0
- package/src/skills/tailwindcss-best-practices/references/transform-translate.md +77 -0
- package/src/skills/tailwindcss-best-practices/references/typography-font-text.md +142 -0
- package/src/skills/tailwindcss-best-practices/references/typography-list-style.md +65 -0
- package/src/skills/tailwindcss-best-practices/references/typography-text-align.md +60 -0
- package/src/skills/tailwindcss-best-practices/references/visual-background.md +76 -0
- package/src/skills/tailwindcss-best-practices/references/visual-border.md +108 -0
- package/src/skills/tailwindcss-best-practices/references/visual-effects.md +111 -0
- package/src/skills/tailwindcss-best-practices/references/visual-svg.md +82 -0
- package/src/skills/test-mobile-app/SKILL.md +11 -6
- package/src/skills/test-mobile-app/scripts/analyze_apk.py +15 -4
- package/src/skills/test-mobile-app/scripts/check_environment.py +5 -5
- package/src/skills/test-mobile-app/scripts/run_tests.py +1 -1
- package/src/skills/test-web-ui/SKILL.md +264 -84
- package/src/skills/test-web-ui/scripts/discover.py +25 -12
- package/src/skills/test-web-ui/scripts/run_tests.py +3 -2
- package/src/skills/tm-search/SKILL.md +242 -106
- package/src/skills/tm-search/references/scraping-fallback.md +60 -95
- package/src/skills/tm-search/scripts/tm_search.py +453 -375
- package/src/skills/vite-best-practices/SKILL.md +115 -0
- package/src/skills/vite-best-practices/references/build-and-ssr.md +255 -0
- package/src/skills/vite-best-practices/references/core-config.md +231 -0
- package/src/skills/vite-best-practices/references/core-features.md +222 -0
- package/src/skills/vite-best-practices/references/core-plugin-api.md +294 -0
- package/src/skills/vite-best-practices/references/environment-api.md +108 -0
- package/src/skills/vite-best-practices/references/rolldown-migration.md +242 -0
- package/src/skills/screen-recording/references/approach2-xvfb.md +0 -232
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
# FFmpeg Screen Recording — Platform Reference
|
|
2
|
+
|
|
3
|
+
Complete command reference for recording the actual screen with FFmpeg.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Platform Detection](#platform-detection)
|
|
7
|
+
- [Windows (gdigrab)](#windows-gdigrab)
|
|
8
|
+
- [macOS (avfoundation)](#macos-avfoundation)
|
|
9
|
+
- [Linux X11 (x11grab)](#linux-x11-x11grab)
|
|
10
|
+
- [Linux Wayland](#linux-wayland)
|
|
11
|
+
- [Audio Capture](#audio-capture)
|
|
12
|
+
- [Window Capture](#window-capture)
|
|
13
|
+
- [Quality & Performance Tuning](#quality--performance-tuning)
|
|
14
|
+
- [Full Cross-Platform Script](#full-cross-platform-script)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Platform Detection
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import platform, shutil, os
|
|
22
|
+
|
|
23
|
+
def detect_recording_backend():
|
|
24
|
+
"""Detect the best FFmpeg input format for this platform."""
|
|
25
|
+
system = platform.system()
|
|
26
|
+
has_ffmpeg = shutil.which("ffmpeg") is not None
|
|
27
|
+
|
|
28
|
+
if not has_ffmpeg:
|
|
29
|
+
return "python_fallback"
|
|
30
|
+
|
|
31
|
+
if system == "Windows":
|
|
32
|
+
return "gdigrab"
|
|
33
|
+
elif system == "Darwin":
|
|
34
|
+
return "avfoundation"
|
|
35
|
+
else: # Linux
|
|
36
|
+
if os.environ.get("WAYLAND_DISPLAY"):
|
|
37
|
+
if shutil.which("wf-recorder"):
|
|
38
|
+
return "wf-recorder"
|
|
39
|
+
return "x11grab" # may not work on Wayland, but try
|
|
40
|
+
return "x11grab"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Windows (gdigrab)
|
|
46
|
+
|
|
47
|
+
### Full screen
|
|
48
|
+
```bash
|
|
49
|
+
ffmpeg -f gdigrab -framerate 30 -draw_mouse 1 -i desktop ^
|
|
50
|
+
-c:v libx264 -preset ultrafast -crf 23 -pix_fmt yuv420p ^
|
|
51
|
+
recording.mp4 -y
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Specific region (x=100, y=200, 800x600)
|
|
55
|
+
```bash
|
|
56
|
+
ffmpeg -f gdigrab -framerate 30 -draw_mouse 1 ^
|
|
57
|
+
-offset_x 100 -offset_y 200 -video_size 800x600 ^
|
|
58
|
+
-i desktop -c:v libx264 -preset ultrafast -crf 23 -pix_fmt yuv420p ^
|
|
59
|
+
recording.mp4 -y
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Specific window by title
|
|
63
|
+
```bash
|
|
64
|
+
ffmpeg -f gdigrab -framerate 30 -draw_mouse 1 ^
|
|
65
|
+
-i title="Untitled - Notepad" ^
|
|
66
|
+
-c:v libx264 -preset ultrafast -crf 23 -pix_fmt yuv420p ^
|
|
67
|
+
recording.mp4 -y
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### With audio (system sound)
|
|
71
|
+
```bash
|
|
72
|
+
ffmpeg -f dshow -i audio="Stereo Mix" ^
|
|
73
|
+
-f gdigrab -framerate 30 -draw_mouse 1 -i desktop ^
|
|
74
|
+
-c:v libx264 -preset ultrafast -crf 23 -pix_fmt yuv420p ^
|
|
75
|
+
-c:a aac -b:a 128k ^
|
|
76
|
+
recording.mp4 -y
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### List available audio devices
|
|
80
|
+
```bash
|
|
81
|
+
ffmpeg -list_devices true -f dshow -i dummy 2>&1 | findstr "audio"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
> **Note**: "Stereo Mix" must be enabled in Windows Sound settings → Recording tab → right-click → Show Disabled Devices → Enable "Stereo Mix". If unavailable, use virtual audio cable software.
|
|
85
|
+
|
|
86
|
+
### With fixed duration (30 seconds)
|
|
87
|
+
```bash
|
|
88
|
+
ffmpeg -f gdigrab -framerate 30 -draw_mouse 1 -t 30 -i desktop ^
|
|
89
|
+
-c:v libx264 -preset ultrafast -crf 23 -pix_fmt yuv420p ^
|
|
90
|
+
recording.mp4 -y
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## macOS (avfoundation)
|
|
96
|
+
|
|
97
|
+
### List available devices
|
|
98
|
+
```bash
|
|
99
|
+
ffmpeg -f avfoundation -list_devices true -i "" 2>&1
|
|
100
|
+
```
|
|
101
|
+
Output shows numbered screen and audio devices. Typically: `[0]` = screen, `[1]` = microphone.
|
|
102
|
+
|
|
103
|
+
### Full screen (no audio)
|
|
104
|
+
```bash
|
|
105
|
+
ffmpeg -f avfoundation -framerate 30 -i "0" \
|
|
106
|
+
-c:v libx264 -preset ultrafast -crf 23 -pix_fmt yuv420p \
|
|
107
|
+
recording.mp4 -y
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Full screen with audio
|
|
111
|
+
```bash
|
|
112
|
+
ffmpeg -f avfoundation -framerate 30 -i "0:1" \
|
|
113
|
+
-c:v libx264 -preset ultrafast -crf 23 -pix_fmt yuv420p \
|
|
114
|
+
-c:a aac -b:a 128k \
|
|
115
|
+
recording.mp4 -y
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Region capture (crop filter)
|
|
119
|
+
```bash
|
|
120
|
+
# Record full screen then crop to 800x600 at offset (100, 200)
|
|
121
|
+
ffmpeg -f avfoundation -framerate 30 -i "0" \
|
|
122
|
+
-vf "crop=800:600:100:200" \
|
|
123
|
+
-c:v libx264 -preset ultrafast -crf 23 -pix_fmt yuv420p \
|
|
124
|
+
recording.mp4 -y
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
> **Note**: avfoundation doesn't support offset/region natively. Use `-vf crop=w:h:x:y` to crop after capture. This captures the full screen and discards the rest — slightly less efficient but works.
|
|
128
|
+
|
|
129
|
+
### Cursor
|
|
130
|
+
macOS avfoundation captures the cursor by default. To hide it, use `-capture_cursor 0`.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Linux X11 (x11grab)
|
|
135
|
+
|
|
136
|
+
### Full screen
|
|
137
|
+
```bash
|
|
138
|
+
ffmpeg -f x11grab -framerate 30 -draw_mouse 1 \
|
|
139
|
+
-i :0.0 \
|
|
140
|
+
-c:v libx264 -preset ultrafast -crf 23 -pix_fmt yuv420p \
|
|
141
|
+
recording.mp4 -y
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Specific region (800x600 at offset 100,200)
|
|
145
|
+
```bash
|
|
146
|
+
ffmpeg -f x11grab -framerate 30 -draw_mouse 1 \
|
|
147
|
+
-video_size 800x600 -i :0.0+100,200 \
|
|
148
|
+
-c:v libx264 -preset ultrafast -crf 23 -pix_fmt yuv420p \
|
|
149
|
+
recording.mp4 -y
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### With audio (PulseAudio)
|
|
153
|
+
```bash
|
|
154
|
+
ffmpeg -f pulse -i default \
|
|
155
|
+
-f x11grab -framerate 30 -draw_mouse 1 -i :0.0 \
|
|
156
|
+
-c:v libx264 -preset ultrafast -crf 23 -pix_fmt yuv420p \
|
|
157
|
+
-c:a aac -b:a 128k \
|
|
158
|
+
recording.mp4 -y
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### With audio (ALSA)
|
|
162
|
+
```bash
|
|
163
|
+
ffmpeg -f alsa -i default \
|
|
164
|
+
-f x11grab -framerate 30 -draw_mouse 1 -i :0.0 \
|
|
165
|
+
-c:v libx264 -preset ultrafast -crf 23 -pix_fmt yuv420p \
|
|
166
|
+
-c:a aac -b:a 128k \
|
|
167
|
+
recording.mp4 -y
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Get screen resolution
|
|
171
|
+
```bash
|
|
172
|
+
xdpyinfo | grep dimensions
|
|
173
|
+
# or
|
|
174
|
+
xrandr | grep '*'
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Linux Wayland
|
|
180
|
+
|
|
181
|
+
FFmpeg's x11grab does **not** work on Wayland. Use `wf-recorder` instead:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# Full screen
|
|
185
|
+
wf-recorder -f recording.mp4
|
|
186
|
+
|
|
187
|
+
# With audio
|
|
188
|
+
wf-recorder -a -f recording.mp4
|
|
189
|
+
|
|
190
|
+
# Specific region (interactive selection)
|
|
191
|
+
wf-recorder -g "$(slurp)" -f recording.mp4
|
|
192
|
+
|
|
193
|
+
# Fixed region
|
|
194
|
+
wf-recorder -g "100,200 800x600" -f recording.mp4
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Install**: `sudo apt install wf-recorder` (Ubuntu) or `sudo pacman -S wf-recorder` (Arch)
|
|
198
|
+
|
|
199
|
+
> **Detection**: Check `echo $WAYLAND_DISPLAY` — if set, you're on Wayland.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Audio Capture
|
|
204
|
+
|
|
205
|
+
### Detect available audio devices
|
|
206
|
+
|
|
207
|
+
**Windows:**
|
|
208
|
+
```python
|
|
209
|
+
import subprocess
|
|
210
|
+
result = subprocess.run(
|
|
211
|
+
["ffmpeg", "-list_devices", "true", "-f", "dshow", "-i", "dummy"],
|
|
212
|
+
capture_output=True, text=True
|
|
213
|
+
)
|
|
214
|
+
# Parse stderr for audio device names
|
|
215
|
+
for line in result.stderr.splitlines():
|
|
216
|
+
if "audio" in line.lower():
|
|
217
|
+
print(line.strip())
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**macOS:**
|
|
221
|
+
```python
|
|
222
|
+
result = subprocess.run(
|
|
223
|
+
["ffmpeg", "-f", "avfoundation", "-list_devices", "true", "-i", ""],
|
|
224
|
+
capture_output=True, text=True
|
|
225
|
+
)
|
|
226
|
+
for line in result.stderr.splitlines():
|
|
227
|
+
print(line.strip())
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Linux:**
|
|
231
|
+
```bash
|
|
232
|
+
# PulseAudio
|
|
233
|
+
pactl list short sources
|
|
234
|
+
|
|
235
|
+
# ALSA
|
|
236
|
+
arecord -L
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Audio device selection in Python
|
|
240
|
+
```python
|
|
241
|
+
def get_audio_device():
|
|
242
|
+
system = platform.system()
|
|
243
|
+
if system == "Windows":
|
|
244
|
+
return "Stereo Mix" # Common default; detect with dshow list
|
|
245
|
+
elif system == "Darwin":
|
|
246
|
+
return "1" # Audio device index from avfoundation list
|
|
247
|
+
else:
|
|
248
|
+
return "default" # PulseAudio default source
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Window Capture
|
|
254
|
+
|
|
255
|
+
### Windows — by window title
|
|
256
|
+
```python
|
|
257
|
+
# FFmpeg gdigrab supports -i title="Window Title"
|
|
258
|
+
cmd = ["ffmpeg", "-f", "gdigrab", "-framerate", "30",
|
|
259
|
+
"-i", f'title={window_title}',
|
|
260
|
+
"-c:v", "libx264", "-preset", "ultrafast", output, "-y"]
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### macOS — by window (not directly supported)
|
|
264
|
+
macOS avfoundation captures screens, not windows. Workaround:
|
|
265
|
+
1. Get window bounds with `osascript` (AppleScript)
|
|
266
|
+
2. Record full screen with crop filter
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
import subprocess, json
|
|
270
|
+
|
|
271
|
+
def get_macos_window_bounds(app_name):
|
|
272
|
+
script = f'''
|
|
273
|
+
tell application "System Events"
|
|
274
|
+
tell process "{app_name}"
|
|
275
|
+
set pos to position of front window
|
|
276
|
+
set sz to size of front window
|
|
277
|
+
return (item 1 of pos) & "," & (item 2 of pos) & "," & (item 1 of sz) & "," & (item 2 of sz)
|
|
278
|
+
end tell
|
|
279
|
+
end tell
|
|
280
|
+
'''
|
|
281
|
+
result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True)
|
|
282
|
+
x, y, w, h = result.stdout.strip().split(",")
|
|
283
|
+
return int(x), int(y), int(w), int(h)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Linux X11 — by window ID
|
|
287
|
+
```bash
|
|
288
|
+
# Get window ID interactively (click on window)
|
|
289
|
+
xdotool selectwindow
|
|
290
|
+
|
|
291
|
+
# Get window geometry by ID
|
|
292
|
+
xdotool getwindowgeometry --shell WINDOW_ID
|
|
293
|
+
|
|
294
|
+
# Get by name
|
|
295
|
+
xdotool search --name "Firefox" | head -1
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
import subprocess
|
|
300
|
+
|
|
301
|
+
def get_x11_window_geometry(window_name):
|
|
302
|
+
wid = subprocess.run(
|
|
303
|
+
["xdotool", "search", "--name", window_name],
|
|
304
|
+
capture_output=True, text=True
|
|
305
|
+
).stdout.strip().split('\n')[0]
|
|
306
|
+
|
|
307
|
+
geo = subprocess.run(
|
|
308
|
+
["xdotool", "getwindowgeometry", "--shell", wid],
|
|
309
|
+
capture_output=True, text=True
|
|
310
|
+
).stdout
|
|
311
|
+
# Parse X, Y, WIDTH, HEIGHT from output
|
|
312
|
+
vals = {}
|
|
313
|
+
for line in geo.strip().split('\n'):
|
|
314
|
+
k, v = line.split('=')
|
|
315
|
+
vals[k] = int(v)
|
|
316
|
+
return vals['X'], vals['Y'], vals['WIDTH'], vals['HEIGHT']
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Quality & Performance Tuning
|
|
322
|
+
|
|
323
|
+
### Encoding presets (speed vs size)
|
|
324
|
+
| Preset | Speed | File Size | Use Case |
|
|
325
|
+
|--------|-------|-----------|----------|
|
|
326
|
+
| `ultrafast` | Fastest | Largest | Live recording (recommended) |
|
|
327
|
+
| `superfast` | Very fast | Large | Recording with modest CPU |
|
|
328
|
+
| `fast` | Fast | Medium | Post-recording re-encode |
|
|
329
|
+
| `medium` | Moderate | Smaller | Final output for sharing |
|
|
330
|
+
| `slow` | Slow | Smallest | Archival quality |
|
|
331
|
+
|
|
332
|
+
### CRF values (quality vs size)
|
|
333
|
+
| CRF | Quality | Use Case |
|
|
334
|
+
|-----|---------|----------|
|
|
335
|
+
| 18 | Visually lossless | Maximum quality |
|
|
336
|
+
| 23 | High (default) | Good balance |
|
|
337
|
+
| 28 | Medium | Smaller files, acceptable quality |
|
|
338
|
+
| 35 | Low | Minimum usable quality |
|
|
339
|
+
|
|
340
|
+
### Recommended settings
|
|
341
|
+
```bash
|
|
342
|
+
# During recording (prioritize speed)
|
|
343
|
+
-preset ultrafast -crf 23
|
|
344
|
+
|
|
345
|
+
# Re-encode after recording (prioritize size)
|
|
346
|
+
ffmpeg -i recording.mp4 -c:v libx264 -preset slow -crf 23 compressed.mp4
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### FPS recommendations
|
|
350
|
+
- `30` — Standard, smooth motion (default)
|
|
351
|
+
- `24` — Cinematic feel, smaller files
|
|
352
|
+
- `15` — Acceptable for static content (presentations, text-heavy screens)
|
|
353
|
+
- `60` — Smooth for fast-moving content (games, animations)
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Full Cross-Platform Script
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
#!/usr/bin/env python3
|
|
361
|
+
"""
|
|
362
|
+
Cross-platform screen recorder using FFmpeg.
|
|
363
|
+
Usage: python3 record.py [output.mp4] [--duration 30] [--fps 30] [--audio] [--region x,y,w,h]
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
import platform, subprocess, shutil, signal, os, sys
|
|
367
|
+
|
|
368
|
+
def build_ffmpeg_cmd(output, duration=None, fps=30, region=None, audio=False, window=None):
|
|
369
|
+
system = platform.system()
|
|
370
|
+
if not shutil.which("ffmpeg"):
|
|
371
|
+
raise RuntimeError("FFmpeg not found. See: https://ffmpeg.org/download.html")
|
|
372
|
+
|
|
373
|
+
cmd = ["ffmpeg"]
|
|
374
|
+
|
|
375
|
+
if system == "Windows":
|
|
376
|
+
if audio:
|
|
377
|
+
cmd += ["-f", "dshow", "-i", "audio=Stereo Mix"]
|
|
378
|
+
cmd += ["-f", "gdigrab", "-framerate", str(fps), "-draw_mouse", "1"]
|
|
379
|
+
if window:
|
|
380
|
+
cmd += ["-i", f"title={window}"]
|
|
381
|
+
elif region:
|
|
382
|
+
x, y, w, h = region
|
|
383
|
+
cmd += ["-offset_x", str(x), "-offset_y", str(y),
|
|
384
|
+
"-video_size", f"{w}x{h}", "-i", "desktop"]
|
|
385
|
+
else:
|
|
386
|
+
cmd += ["-i", "desktop"]
|
|
387
|
+
|
|
388
|
+
elif system == "Darwin":
|
|
389
|
+
devices = "0" if not audio else "0:1"
|
|
390
|
+
cmd += ["-f", "avfoundation", "-framerate", str(fps),
|
|
391
|
+
"-capture_cursor", "1", "-i", devices]
|
|
392
|
+
if region:
|
|
393
|
+
x, y, w, h = region
|
|
394
|
+
cmd += ["-vf", f"crop={w}:{h}:{x}:{y}"]
|
|
395
|
+
|
|
396
|
+
else: # Linux
|
|
397
|
+
if audio:
|
|
398
|
+
cmd += ["-f", "pulse", "-i", "default"]
|
|
399
|
+
display = os.environ.get("DISPLAY", ":0.0")
|
|
400
|
+
cmd += ["-f", "x11grab", "-framerate", str(fps), "-draw_mouse", "1"]
|
|
401
|
+
if region:
|
|
402
|
+
x, y, w, h = region
|
|
403
|
+
cmd += ["-video_size", f"{w}x{h}", "-i", f"{display}+{x},{y}"]
|
|
404
|
+
else:
|
|
405
|
+
cmd += ["-i", display]
|
|
406
|
+
|
|
407
|
+
cmd += ["-c:v", "libx264", "-preset", "ultrafast",
|
|
408
|
+
"-crf", "23", "-pix_fmt", "yuv420p"]
|
|
409
|
+
if audio:
|
|
410
|
+
cmd += ["-c:a", "aac", "-b:a", "128k"]
|
|
411
|
+
if duration:
|
|
412
|
+
cmd += ["-t", str(duration)]
|
|
413
|
+
cmd += [output, "-y"]
|
|
414
|
+
return cmd
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def record_screen(output="recording.mp4", duration=None, fps=30,
|
|
418
|
+
region=None, audio=False, window=None):
|
|
419
|
+
"""Record the screen to an MP4 file."""
|
|
420
|
+
system = platform.system()
|
|
421
|
+
|
|
422
|
+
# Wayland special case
|
|
423
|
+
if system == "Linux" and os.environ.get("WAYLAND_DISPLAY"):
|
|
424
|
+
if shutil.which("wf-recorder"):
|
|
425
|
+
cmd = ["wf-recorder", "-f", output]
|
|
426
|
+
if audio:
|
|
427
|
+
cmd.append("-a")
|
|
428
|
+
if region:
|
|
429
|
+
x, y, w, h = region
|
|
430
|
+
cmd += ["-g", f"{x},{y} {w}x{h}"]
|
|
431
|
+
if duration:
|
|
432
|
+
# wf-recorder doesn't have -t; use timeout
|
|
433
|
+
cmd = ["timeout", str(duration)] + cmd
|
|
434
|
+
print(f"Recording (wf-recorder) → {output}")
|
|
435
|
+
proc = subprocess.Popen(cmd)
|
|
436
|
+
try:
|
|
437
|
+
proc.wait()
|
|
438
|
+
except KeyboardInterrupt:
|
|
439
|
+
proc.send_signal(signal.SIGINT)
|
|
440
|
+
proc.wait()
|
|
441
|
+
print(f"Saved: {output} ({os.path.getsize(output):,} bytes)")
|
|
442
|
+
return output
|
|
443
|
+
|
|
444
|
+
cmd = build_ffmpeg_cmd(output, duration, fps, region, audio, window)
|
|
445
|
+
stop_msg = f" for {duration}s" if duration else " (Ctrl+C to stop)"
|
|
446
|
+
print(f"Recording{stop_msg} → {output}")
|
|
447
|
+
proc = subprocess.Popen(cmd, stderr=subprocess.PIPE)
|
|
448
|
+
try:
|
|
449
|
+
proc.wait()
|
|
450
|
+
except KeyboardInterrupt:
|
|
451
|
+
proc.send_signal(signal.SIGINT)
|
|
452
|
+
proc.wait()
|
|
453
|
+
size = os.path.getsize(output) if os.path.exists(output) else 0
|
|
454
|
+
print(f"Saved: {output} ({size:,} bytes)")
|
|
455
|
+
return output
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
if __name__ == "__main__":
|
|
459
|
+
import argparse
|
|
460
|
+
parser = argparse.ArgumentParser(description="Record screen to MP4")
|
|
461
|
+
parser.add_argument("output", nargs="?", default="recording.mp4")
|
|
462
|
+
parser.add_argument("--duration", "-t", type=int, default=None)
|
|
463
|
+
parser.add_argument("--fps", type=int, default=30)
|
|
464
|
+
parser.add_argument("--audio", "-a", action="store_true")
|
|
465
|
+
parser.add_argument("--region", "-r", type=str, default=None,
|
|
466
|
+
help="x,y,w,h (e.g. 100,200,800,600)")
|
|
467
|
+
parser.add_argument("--window", "-w", type=str, default=None,
|
|
468
|
+
help="Window title (Windows only)")
|
|
469
|
+
args = parser.parse_args()
|
|
470
|
+
|
|
471
|
+
region = tuple(map(int, args.region.split(","))) if args.region else None
|
|
472
|
+
record_screen(args.output, args.duration, args.fps, region, args.audio, args.window)
|
|
473
|
+
```
|
|
@@ -9,19 +9,19 @@ Full offline pipeline. No browser, no display server needed.
|
|
|
9
9
|
"""
|
|
10
10
|
Autonomous Product Demo Video Generator
|
|
11
11
|
Usage: python3 generate_demo.py
|
|
12
|
-
Output:
|
|
12
|
+
Output: ./demo.mp4 (current working directory)
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
from moviepy import VideoClip, AudioFileClip
|
|
16
16
|
import numpy as np
|
|
17
17
|
from PIL import Image, ImageDraw, ImageFont
|
|
18
|
-
import pyttsx3, subprocess, os,
|
|
18
|
+
import pyttsx3, subprocess, os, tempfile
|
|
19
19
|
|
|
20
20
|
# ── CONFIG ─────────────────────────────────────────────────────────────────────
|
|
21
21
|
WIDTH, HEIGHT = 1280, 720
|
|
22
22
|
FPS = 24
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
TMP = tempfile.gettempdir()
|
|
24
|
+
OUTPUT_PATH = os.path.join(os.getcwd(), "demo.mp4")
|
|
25
25
|
|
|
26
26
|
# Color palette (dark tech theme)
|
|
27
27
|
C = {
|
|
@@ -36,6 +36,20 @@ C = {
|
|
|
36
36
|
"card": (25, 28, 60),
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
# Font handling (falls back to Pillow default if no TTF found)
|
|
40
|
+
def _load_font(size=24):
|
|
41
|
+
for p in ["/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
42
|
+
"/usr/share/fonts/TTF/DejaVuSans.ttf",
|
|
43
|
+
"/System/Library/Fonts/Helvetica.ttc",
|
|
44
|
+
"C:/Windows/Fonts/arial.ttf"]:
|
|
45
|
+
if os.path.exists(p):
|
|
46
|
+
return ImageFont.truetype(p, size)
|
|
47
|
+
return ImageFont.load_default()
|
|
48
|
+
|
|
49
|
+
FONT_LG = _load_font(36)
|
|
50
|
+
FONT_MD = _load_font(24)
|
|
51
|
+
FONT_SM = _load_font(16)
|
|
52
|
+
|
|
39
53
|
# ── SCENES ─────────────────────────────────────────────────────────────────────
|
|
40
54
|
SCENES = [
|
|
41
55
|
{
|
|
@@ -70,9 +84,9 @@ def ease(t, d):
|
|
|
70
84
|
|
|
71
85
|
def draw_header(draw, title, subtitle=""):
|
|
72
86
|
draw.rectangle([0, 0, WIDTH, 72], fill=C["header"])
|
|
73
|
-
draw.text((32, 16), title, fill=C["text"])
|
|
87
|
+
draw.text((32, 16), title, fill=C["text"], font=FONT_MD)
|
|
74
88
|
if subtitle:
|
|
75
|
-
draw.text((32, 46), subtitle, fill=C["subtext"])
|
|
89
|
+
draw.text((32, 46), subtitle, fill=C["subtext"], font=FONT_SM)
|
|
76
90
|
# Header accent line
|
|
77
91
|
draw.rectangle([0, 72, WIDTH, 75], fill=C["accent"])
|
|
78
92
|
|
|
@@ -105,7 +119,8 @@ def draw_intro(draw, t, d):
|
|
|
105
119
|
x = int(WIDTH/2 - len(title)*12)
|
|
106
120
|
y = int(200 + (1 - alpha_title) * 60)
|
|
107
121
|
draw.text((x, y), title, fill=(
|
|
108
|
-
int(255 * alpha_title), int(255 * alpha_title), int(255 * alpha_title))
|
|
122
|
+
int(255 * alpha_title), int(255 * alpha_title), int(255 * alpha_title)),
|
|
123
|
+
font=FONT_LG)
|
|
109
124
|
|
|
110
125
|
# Subtitle
|
|
111
126
|
sub = "Transforming workflows with AI automation"
|
|
@@ -223,15 +238,24 @@ DRAW_FUNCTIONS = {
|
|
|
223
238
|
|
|
224
239
|
# ── MAIN PIPELINE ──────────────────────────────────────────────────────────────
|
|
225
240
|
def generate_video():
|
|
241
|
+
# NOTE: Single TTS call for all scenes. For per-scene timing, generate audio per scene
|
|
242
|
+
# (see SKILL.md "Scene Structure Pattern" section for the per-scene alternative).
|
|
226
243
|
print("📝 Step 1: Generating TTS narration...")
|
|
227
244
|
narration_text = " ".join(s["narration"] for s in SCENES)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
245
|
+
wav_path = os.path.join(TMP, 'narration.wav')
|
|
246
|
+
mp3_path = os.path.join(TMP, 'narration.mp3')
|
|
247
|
+
audio_available = False
|
|
248
|
+
try:
|
|
249
|
+
engine = pyttsx3.init()
|
|
250
|
+
engine.setProperty('rate', 140)
|
|
251
|
+
engine.save_to_file(narration_text, wav_path)
|
|
252
|
+
engine.runAndWait()
|
|
253
|
+
subprocess.run(['ffmpeg', '-i', wav_path, '-c:a', 'libmp3lame',
|
|
254
|
+
'-b:a', '128k', mp3_path, '-y', '-loglevel', 'quiet'])
|
|
255
|
+
audio_available = True
|
|
256
|
+
print(f" ✅ Narration: {os.path.getsize(mp3_path):,} bytes")
|
|
257
|
+
except Exception as e:
|
|
258
|
+
print(f" ⚠️ TTS unavailable ({e}). Generating silent video.")
|
|
235
259
|
|
|
236
260
|
print("🎬 Step 2: Rendering video frames...")
|
|
237
261
|
# Build timeline
|
|
@@ -254,18 +278,17 @@ def generate_video():
|
|
|
254
278
|
print(f" ✅ Clip: {TOTAL_DURATION}s at {FPS}fps")
|
|
255
279
|
|
|
256
280
|
print("🎵 Step 3: Combining video + audio...")
|
|
257
|
-
|
|
258
|
-
|
|
281
|
+
if audio_available:
|
|
282
|
+
audio = AudioFileClip(mp3_path).with_duration(TOTAL_DURATION)
|
|
283
|
+
final = clip.with_audio(audio)
|
|
284
|
+
else:
|
|
285
|
+
final = clip
|
|
259
286
|
final.write_videofile(OUTPUT_PATH, fps=FPS, logger=None)
|
|
260
287
|
size = os.path.getsize(OUTPUT_PATH)
|
|
261
288
|
print(f" ✅ Video: {size:,} bytes ({size//1024} KB)")
|
|
289
|
+
print(f" ✅ Saved to: {OUTPUT_PATH}")
|
|
262
290
|
|
|
263
|
-
|
|
264
|
-
os.makedirs(os.path.dirname(FINAL_OUTPUT), exist_ok=True)
|
|
265
|
-
shutil.copy(OUTPUT_PATH, FINAL_OUTPUT)
|
|
266
|
-
print(f" ✅ Saved to: {FINAL_OUTPUT}")
|
|
267
|
-
|
|
268
|
-
return FINAL_OUTPUT
|
|
291
|
+
return OUTPUT_PATH
|
|
269
292
|
|
|
270
293
|
if __name__ == "__main__":
|
|
271
294
|
result = generate_video()
|