Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Takes an existing screen recording or demo video and adds professional zoom/pan effects synchronized to the narration. Uses transcript-driven zoom targeting and Remotion for rendering. Optionally replaces audio with a soundtrack.
.claude/skills/gooseworks-ai-video-polish/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | 0% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 38% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 173% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 293% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 333% | 0% |
You take an existing video (screen recording, demo, walkthrough, Loom) and add professional zoom/pan effects that follow the narration. The output looks like a professionally edited video where the camera zooms into whatever the speaker is discussing.
Input: A raw video file (screen recording, Loom, product demo) + optionally a soundtrack Output: The same video with smooth zoom/pan effects synchronized to the narration
What it adds:
What it does NOT do:
bash npx create-video@latest --yes --blank --no-tailwind video-polish cd video-polish && npm i
brew install whisper-cpp on macOSbash curl -L -o /tmp/ggml-base.en.bin "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin"
pip install Pillow)Before starting: Verify that Node.js, whisper-cpp, and Python 3 with Pillow are installed. If any are missing, instruct the user to install them before proceeding.
Get video metadata:
bashnpx remotion ffprobe -v quiet -print_format json -show_format -show_streams <video_path>
Record: duration, resolution (width x height), fps, whether audio exists.
Extract audio and transcribe with word-level timestamps.
Extract audio:
bashnpx remotion ffmpeg -y -i <video_path> -vn -acodec pcm_s16le -ar 16000 -ac 1 /tmp/audio.wav
Transcribe:
bashwhisper-cli -m /tmp/ggml-base.en.bin -f /tmp/audio.wav --output-json --output-file /tmp/transcript
Read the transcript and identify moments where the narrator emphasizes or references specific on-screen elements:
For each emphasis moment, record:
For each emphasis moment from the transcript, extract the source frame at that timestamp:
bashnpx remotion ffmpeg -y -i <video_path> -vf "select=eq(n\,<frame_number>)" -vframes 1 -update 1 /tmp/frame-<timestamp>.png
Where frame_number = timestamp_seconds * fps.
Important: The video content may scroll or change over time. Always extract frames at the ACTUAL timestamp, not from a single reference frame. UI element positions change as the user scrolls.
For each extracted frame, draw a coordinate grid overlay to precisely identify element positions:
pythonfrom PIL import Image, ImageDraw img = Image.open('/tmp/frame-<timestamp>.png') w, h = img.size draw = ImageDraw.Draw(img) for pct in range(0, 100, 5): y = int(h * pct / 100) x = int(w * pct / 100) color = 'red' if pct % 10 == 0 else 'yellow' draw.line([(0, y), (w, y)], fill=color, width=1) draw.text((5, y+2), f'y{pct}', fill=color) draw.line([(x, 0), (x, h)], fill=color, width=1) draw.text((x+2, 12), f'x{pct}', fill=color) img.save('/tmp/frame-<timestamp>-grid.png')
Look at the grid overlay and measure the center point (focusX, focusY) of the element the narrator is referring to. Express as normalized 0-1 values (e.g., x=0.47 means 47% from the left edge).
Create a list of keyframes. Each keyframe defines a target zoom state at a specific time. The system smoothly interpolates between consecutive keyframes using ease-in-out.
Keyframe design rules:
Zoom level guide: | What you're showing | Zoom level | |---|---| | Full dashboard/page overview | 1.0 (no zoom) | | A section (metrics row + charts) | 1.1-1.3 | | A specific area (one chart, a few table columns) | 1.8-2.5 | | A single metric, cell, or button | 2.8-3.5 |
Example keyframe timeline:
typescriptconst KEYFRAMES = [ { timeSec: 0, zoom: 1.0, focusX: 0.5, focusY: 0.5 }, // Full view { timeSec: 23, zoom: 1.0, focusX: 0.5, focusY: 0.5 }, // Still full, about to zoom { timeSec: 25, zoom: 1.2, focusX: 0.45, focusY: 0.20 }, // Gentle zoom on metrics area { timeSec: 29, zoom: 1.2, focusX: 0.45, focusY: 0.20 }, // Hold { timeSec: 30.5, zoom: 3.0, focusX: 0.56, focusY: 0.15 }, // Zoom on specific metric { timeSec: 34, zoom: 3.0, focusX: 0.56, focusY: 0.15 }, // Hold { timeSec: 36, zoom: 3.0, focusX: 0.06, focusY: 0.15 }, // Slide to different metric // ... etc ];
Before doing a full render, verify every zoom target with still frames. This is the most important step — it catches coordinate errors that would waste a full render cycle.
For each keyframe where the zoom or focus changes, render a single still frame:
bashnpx remotion still <CompositionId> --frame=<frame_number> --output=/tmp/verify-<timestamp>.png
Self-review each still frame:
Common coordinate mistakes to check for:
Create the Remotion project files. The composition structure:
Root.tsx:
tsximport { Composition } from "remotion"; import { MyComposition } from "./Composition"; export const RemotionRoot: React.FC = () => { return ( <Composition id="VideoPolish" component={MyComposition} durationInFrames={DURATION_SECONDS * FPS} fps={FPS} width={VIDEO_WIDTH} height={VIDEO_HEIGHT} /> ); };
Composition.tsx:
tsximport { AbsoluteFill, Audio, OffthreadVideo, staticFile, useCurrentFrame, useVideoConfig, } from "remotion"; type Keyframe = { timeSec: number; zoom: number; focusX: number; focusY: number; }; const KEYFRAMES: Keyframe[] = [ // ... keyframes from Step 5 ]; // Smooth ease-in-out interpolation function smoothstep(t: number): number { const c = Math.max(0, Math.min(1, t)); return c * c * (3 - 2 * c); } function getStateAtTime(timeSec: number) { if (timeSec <= KEYFRAMES[0].timeSec) return KEYFRAMES[0]; if (timeSec >= KEYFRAMES[KEYFRAMES.length - 1].timeSec) return KEYFRAMES[KEYFRAMES.length - 1]; for (let i = 0; i < KEYFRAMES.length - 1; i++) { const kf0 = KEYFRAMES[i]; const kf1 = KEYFRAMES[i + 1]; if (timeSec >= kf0.timeSec && timeSec <= kf1.timeSec) { const t = (timeSec - kf0.timeSec) / (kf1.timeSec - kf0.timeSec); const e = smoothstep(t); return { zoom: kf0.zoom + (kf1.zoom - kf0.zoom) * e, focusX: kf0.focusX + (kf1.focusX - kf0.focusX) * e, focusY: kf0.focusY + (kf1.focusY - kf0.focusY) * e, }; } } return KEYFRAMES[KEYFRAMES.length - 1]; } export const MyComposition: React.FC = () => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const { zoom, focusX, focusY } = getStateAtTime(frame / fps); return ( <AbsoluteFill style={{ backgroundColor: "black" }}> <AbsoluteFill style={{ transform: `scale(${zoom})`, transformOrigin: `${focusX * 100}% ${focusY * 100}%`, }} > <OffthreadVideo src={staticFile("source.mp4")} style={{ width: "100%", height: "100%", objectFit: "cover" }} /> </AbsoluteFill> {/* Optional: background music */} <Audio src={staticFile("music.mp3")} volume={(f) => { const total = DURATION * fps; if (f < fps) return f / fps; // 1s fade in if (f > total - 3 * fps) return (total - f) / (3 * fps); // 3s fade out return 1; }} /> </AbsoluteFill> ); };
Copy the source video (and optional music) into the Remotion project's public/ folder:
bashcp <source_video> <remotion_project>/public/source.mp4 cp <music_file> <remotion_project>/public/music.mp3 # optional
bashnpx remotion render <CompositionId> --output=<output_path>.mp4
Rendering takes approximately 1-3 minutes for a 60-90 second video at 720p/1080p.
Video polished!
- Duration: [X] seconds
- Resolution: [W]x[H]
- Zoom effects: [N] zoom points
- Audio: [original / replaced with soundtrack / mixed]
- File: [local path]
Want me to adjust any zoom points and re-render?| Input | Required | Formats | Notes | |---|---|---|---| | Source video | Yes | MP4, MOV, WebM | Screen recording, Loom, product demo | | Soundtrack | No | MP3, WAV, AAC | Replaces or mixes with original audio | | Zoom instructions | No | Natural language | "Zoom in when he talks about metrics." If not provided, the skill auto-detects from transcript. | | Specific timestamps | No | "Zoom at 0:15, 0:42" | Overrides auto-detection for specific moments |
| Option | What happens | |---|---| | Keep original (default if no soundtrack provided) | Original narration plays, zoom effects are visual only | | Replace with soundtrack | Original audio removed, soundtrack plays with fade-in/fade-out | | Mix | Soundtrack plays at ~20% volume underneath original narration |
| Property | Value | |---|---| | Format | MP4 (H.264) | | Resolution | Same as source video | | Frame rate | Same as source video | | Easing | Smoothstep (cubic ease-in-out) on all transitions | | Render engine | Remotion (CSS transform-based, sub-pixel precision) |
npx create-video). Subsequent runs reuse the same project — just swap the source video and update keyframes.Other measured skills in the registry, with their headline benchmark lift.