Skip to content

Commit f689e47

Browse files
omitsu-devclaude
authored andcommitted
feat: add 10 new commands — equalize, stack, detect-silence, extract-subtitle, audio-delay, deflicker, noise, vintage, zoom, interpolate (v1.8.0)
New commands: - equalize: Auto-correct colors via histogram equalization - stack: Side-by-side or top/bottom two videos - detect-silence: Find silent sections in audio/video - extract-subtitle: Extract subtitle track as .srt file - audio-delay: Fix audio sync by delaying/advancing audio - deflicker: Remove flicker from timelapse videos - noise: Add film grain/noise for cinematic look - vintage: Retro film effect (sepia + grain + vignette) - zoom: Ken Burns style zoom and pan - interpolate: Frame interpolation for smooth slow motion https://claude.ai/code/session_01BPZRs1uut8bAVcEQsjWjsw
1 parent 4737b4d commit f689e47

13 files changed

Lines changed: 295 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ jobs:
8484
node bin/ffmpeg-quick.js tile test.mp4 --dry-run
8585
node bin/ffmpeg-quick.js metadata test.mp4 --title Test --dry-run
8686
node bin/ffmpeg-quick.js drawbox test.mp4 100:50:200:150 --dry-run
87+
node bin/ffmpeg-quick.js equalize test.mp4 --dry-run
88+
node bin/ffmpeg-quick.js stack test.mp4 test.mp4 --dry-run
89+
node bin/ffmpeg-quick.js detect-silence test.mp4 --dry-run
90+
node bin/ffmpeg-quick.js extract-subtitle test.mp4 --dry-run
91+
node bin/ffmpeg-quick.js audio-delay test.mp4 500 --dry-run
92+
node bin/ffmpeg-quick.js deflicker test.mp4 --dry-run
93+
node bin/ffmpeg-quick.js noise test.mp4 --dry-run
94+
node bin/ffmpeg-quick.js vintage test.mp4 --dry-run
95+
node bin/ffmpeg-quick.js zoom test.mp4 --dry-run
96+
node bin/ffmpeg-quick.js interpolate test.mp4 60 --dry-run
8797
8898
- name: Test real encode (compress a generated video)
8999
run: |
@@ -123,3 +133,8 @@ jobs:
123133
node bin/ffmpeg-quick.js metadata test-input.mp4 --title "Test Video" -y
124134
node bin/ffmpeg-quick.js drawbox test-input.mp4 10:10:100:80 -y
125135
node bin/ffmpeg-quick.js crossfade test-input.mp4 test-input.mp4 0.5 -y
136+
node bin/ffmpeg-quick.js equalize test-input.mp4 -y
137+
node bin/ffmpeg-quick.js stack test-input.mp4 test-input.mp4 -y
138+
node bin/ffmpeg-quick.js deflicker test-input.mp4 -y
139+
node bin/ffmpeg-quick.js noise test-input.mp4 -y
140+
node bin/ffmpeg-quick.js vintage test-input.mp4 -y

bin/ffmpeg-quick.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,23 @@ import { register as crossfade } from "../src/commands/crossfade.js";
5757
import { register as tile } from "../src/commands/tile.js";
5858
import { register as metadata } from "../src/commands/metadata.js";
5959
import { register as drawbox } from "../src/commands/drawbox.js";
60+
import { register as equalize } from "../src/commands/equalize.js";
61+
import { register as stack } from "../src/commands/stack.js";
62+
import { register as detectSilence } from "../src/commands/detect-silence.js";
63+
import { register as extractSubtitle } from "../src/commands/extract-subtitle.js";
64+
import { register as audioDelay } from "../src/commands/audio-delay.js";
65+
import { register as deflicker } from "../src/commands/deflicker.js";
66+
import { register as noise } from "../src/commands/noise.js";
67+
import { register as vintage } from "../src/commands/vintage.js";
68+
import { register as zoom } from "../src/commands/zoom.js";
69+
import { register as interpolate } from "../src/commands/interpolate.js";
6070

6171
const program = new Command();
6272

6373
program
6474
.name("ffmpeg-quick")
6575
.description("Quick FFmpeg presets for common video tasks")
66-
.version("1.7.0");
76+
.version("1.8.0");
6777

6878
compress(program);
6979
gif(program);
@@ -121,5 +131,15 @@ crossfade(program);
121131
tile(program);
122132
metadata(program);
123133
drawbox(program);
134+
equalize(program);
135+
stack(program);
136+
detectSilence(program);
137+
extractSubtitle(program);
138+
audioDelay(program);
139+
deflicker(program);
140+
noise(program);
141+
vintage(program);
142+
zoom(program);
143+
interpolate(program);
124144

125145
program.parse();

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ffmpeg-quick",
3-
"version": "1.7.0",
3+
"version": "1.8.0",
44
"description": "Quick FFmpeg presets for common video tasks / FFmpegプリセットCLI",
55
"type": "module",
66
"bin": {

src/commands/audio-delay.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { run } from "../run.js";
2+
import { outputName } from "../utils.js";
3+
4+
export function register(program) {
5+
program
6+
.command("audio-delay")
7+
.description("Delay or advance audio track (fix audio sync)")
8+
.argument("<input>", "Input video file")
9+
.argument("<ms>", "Delay in milliseconds (positive = delay audio, negative = advance)")
10+
.option("-o, --output <path>", "Output file path")
11+
.option("--dry-run", "Print the FFmpeg command without running it")
12+
.option("-y", "Overwrite output without asking")
13+
.action((input, ms, opts) => {
14+
const delay = parseInt(ms, 10);
15+
if (isNaN(delay)) {
16+
console.error("Error: delay must be a number in milliseconds.");
17+
process.exit(1);
18+
}
19+
20+
const out = opts.output || outputName(input, "synced");
21+
const args = ["-i", input];
22+
23+
if (delay >= 0) {
24+
args.push("-af", `adelay=${delay}|${delay}`, "-c:v", "copy");
25+
} else {
26+
// Negative = advance audio by trimming the start
27+
const trimSec = Math.abs(delay) / 1000;
28+
args.push("-af", `atrim=start=${trimSec},asetpts=PTS-STARTPTS`, "-c:v", "copy");
29+
}
30+
31+
if (opts.y) args.push("-y");
32+
args.push(out);
33+
run(args, { dryRun: opts.dryRun });
34+
});
35+
}

src/commands/deflicker.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { run } from "../run.js";
2+
import { outputName } from "../utils.js";
3+
4+
export function register(program) {
5+
program
6+
.command("deflicker")
7+
.description("Remove flicker from video (useful for timelapse)")
8+
.argument("<input>", "Input video file")
9+
.option("--size <n>", "Averaging window size (must be odd)", "5")
10+
.option("-o, --output <path>", "Output file path")
11+
.option("--dry-run", "Print the FFmpeg command without running it")
12+
.option("-y", "Overwrite output without asking")
13+
.action((input, opts) => {
14+
const out = opts.output || outputName(input, "deflickered");
15+
const args = ["-i", input, "-vf", `deflicker=size=${opts.size}`, "-c:a", "copy"];
16+
17+
if (opts.y) args.push("-y");
18+
args.push(out);
19+
run(args, { dryRun: opts.dryRun });
20+
});
21+
}

src/commands/detect-silence.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { run } from "../run.js";
2+
3+
export function register(program) {
4+
program
5+
.command("detect-silence")
6+
.description("Detect silent sections in audio/video")
7+
.argument("<input>", "Input video/audio file")
8+
.option("--threshold <dB>", "Silence threshold in dB", "-30")
9+
.option("--duration <sec>", "Minimum silence duration in seconds", "2")
10+
.option("--dry-run", "Print the FFmpeg command without running it")
11+
.action((input, opts) => {
12+
const filter = `silencedetect=noise=${opts.threshold}dB:d=${opts.duration}`;
13+
const args = ["-i", input, "-af", filter, "-f", "null", "-"];
14+
15+
run(args, { dryRun: opts.dryRun });
16+
});
17+
}

src/commands/equalize.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { run } from "../run.js";
2+
import { outputName } from "../utils.js";
3+
4+
export function register(program) {
5+
program
6+
.command("equalize")
7+
.description("Auto-correct colors with histogram equalization")
8+
.argument("<input>", "Input video file")
9+
.option("-o, --output <path>", "Output file path")
10+
.option("--dry-run", "Print the FFmpeg command without running it")
11+
.option("-y", "Overwrite output without asking")
12+
.action((input, opts) => {
13+
const out = opts.output || outputName(input, "equalized");
14+
const args = ["-i", input, "-vf", "histeq", "-c:a", "copy"];
15+
16+
if (opts.y) args.push("-y");
17+
args.push(out);
18+
run(args, { dryRun: opts.dryRun });
19+
});
20+
}

src/commands/extract-subtitle.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { run } from "../run.js";
2+
import { outputName } from "../utils.js";
3+
4+
export function register(program) {
5+
program
6+
.command("extract-subtitle")
7+
.description("Extract subtitle track from video as .srt file")
8+
.argument("<input>", "Input video file")
9+
.option("--stream <n>", "Subtitle stream index", "0")
10+
.option("-o, --output <path>", "Output file path")
11+
.option("--dry-run", "Print the FFmpeg command without running it")
12+
.option("-y", "Overwrite output without asking")
13+
.action((input, opts) => {
14+
const out = opts.output || outputName(input, "sub", ".srt");
15+
const args = ["-i", input, "-map", `0:s:${opts.stream}`, "-c:s", "srt"];
16+
17+
if (opts.y) args.push("-y");
18+
args.push(out);
19+
run(args, { dryRun: opts.dryRun });
20+
});
21+
}

src/commands/interpolate.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { run } from "../run.js";
2+
import { outputName, parsePositiveNumber } from "../utils.js";
3+
4+
export function register(program) {
5+
program
6+
.command("interpolate")
7+
.description("Frame interpolation for smooth slow motion")
8+
.argument("<input>", "Input video file")
9+
.argument("<target-fps>", "Target frame rate (e.g. 60, 120)")
10+
.option("-o, --output <path>", "Output file path")
11+
.option("--dry-run", "Print the FFmpeg command without running it")
12+
.option("-y", "Overwrite output without asking")
13+
.action((input, targetFps, opts) => {
14+
const fps = parsePositiveNumber(targetFps, "target FPS");
15+
const filter = `minterpolate=fps=${fps}:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1`;
16+
const out = opts.output || outputName(input, `${fps}fps-interp`);
17+
const args = ["-i", input, "-vf", filter, "-c:a", "copy"];
18+
19+
if (opts.y) args.push("-y");
20+
args.push(out);
21+
run(args, { dryRun: opts.dryRun });
22+
});
23+
}

src/commands/noise.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { run } from "../run.js";
2+
import { outputName } from "../utils.js";
3+
4+
export function register(program) {
5+
program
6+
.command("noise")
7+
.description("Add film grain / noise to video")
8+
.argument("<input>", "Input video file")
9+
.option("--strength <n>", "Noise intensity (1-100)", "20")
10+
.option("--type <type>", "Noise type: uniform or gaussian", "gaussian")
11+
.option("-o, --output <path>", "Output file path")
12+
.option("--dry-run", "Print the FFmpeg command without running it")
13+
.option("-y", "Overwrite output without asking")
14+
.action((input, opts) => {
15+
const flags = opts.type === "uniform" ? "u" : "a";
16+
const filter = `noise=alls=${opts.strength}:allf=t+${flags}`;
17+
const out = opts.output || outputName(input, "noisy");
18+
const args = ["-i", input, "-vf", filter, "-c:a", "copy"];
19+
20+
if (opts.y) args.push("-y");
21+
args.push(out);
22+
run(args, { dryRun: opts.dryRun });
23+
});
24+
}

0 commit comments

Comments
 (0)