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.
npx skills add https://github.com/gooseworks-ai/goose-skills --skill video-polish
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:
npx create-video@latest --yes --blank --no-tailwind video-polish
cd video-polish && npm i
brew install whisper-cpp on macOS 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:
npx 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:
npx remotion ffmpeg -y -i <video_path> -vn -acodec pcm_s16le -ar 16000 -ac 1 /tmp/audio.wav
Transcribe:
whisper-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:
npx 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:
from 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:
const 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:
npx 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:
import { 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:
import {
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:
cp <source_video> <remotion_project>/public/source.mp4
cp <music_file> <remotion_project>/public/music.mp3 # optional
npx 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.Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.
Improves the quality of images, especially screenshots, by enhancing resolution, sharpness, and clarity. Perfect for preparing images for presentations, documentation, or social media posts.
Downloads videos from YouTube and other platforms for offline viewing, editing, or archival. Handles various formats and quality options.
Lightweight WSI tile extraction and preprocessing. Use for basic slide processing tissue detection, tile extraction, stain normalization for H&E images. Best for simple pipelines, dataset preparation, quick tile-based analysis. For advanced spatial proteomics, multiplexed imaging, or deep learning pipelines use pathml.
Microscopy data management platform. Access images via Python, retrieve datasets, analyze pixels, manage ROIs/annotations, batch processing, for high-content screening and microscopy workflows.
Python library for working with DICOM (Digital Imaging and Communications in Medicine) files. Use this skill when reading, writing, or modifying medical imaging data in DICOM format, extracting pixel data from medical images (CT, MRI, X-ray, ultrasound), anonymizing DICOM files, working with DICOM metadata and tags, converting DICOM images to other formats, handling compressed DICOM data, or processing medical imaging datasets. Applies to tasks involving medical image analysis, PACS systems, radiology workflows, and healthcare imaging applications.
This skill should be used when working with pre-trained transformer models for natural language processing, computer vision, audio, or multimodal tasks. Use for text generation, classification, question answering, translation, summarization, image classification, object detection, speech recognition, and fine-tuning models on custom datasets.
Take gooseworks-ai/video-polish from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference pip, npx, brew.
Without those the skill loads but fails at the first command.