Skip to content

Commit 0f2c19e

Browse files
omitsu-devclaude
authored andcommitted
feat: add 8 new commands — audio-pitch, audio-eq, pan-audio, trim-silence, gif-to-video, progress, backdrop, count-frames (v2.1.0)
91 total commands. New commands: - audio-pitch: Change pitch without changing speed (rubberband) - audio-eq: Bass/treble/mid equalizer - pan-audio: Stereo panning (left/right positioning) - trim-silence: Auto-remove silent sections - gif-to-video: Convert GIF to MP4 - progress: Add animated progress bar overlay - backdrop: Blurred background for vertical videos (SNS-ready) - count-frames: Count total frames in a video https://claude.ai/code/session_01BPZRs1uut8bAVcEQsjWjsw
1 parent 2cdf89d commit 0f2c19e

11 files changed

Lines changed: 265 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,19 @@ jobs:
111111
node bin/ffmpeg-quick.js timecode test.mp4 --dry-run
112112
node bin/ffmpeg-quick.js compare test.mp4 test.mp4 --dry-run
113113
node bin/ffmpeg-quick.js concat-audio test.mp3 test.mp3 --dry-run
114+
node bin/ffmpeg-quick.js audio-pitch test.mp4 2 --dry-run
115+
node bin/ffmpeg-quick.js audio-eq test.mp4 --bass 5 --dry-run
116+
node bin/ffmpeg-quick.js pan-audio test.mp4 -0.5 --dry-run
117+
node bin/ffmpeg-quick.js trim-silence test.mp4 --dry-run
118+
node bin/ffmpeg-quick.js gif-to-video test.gif --dry-run
119+
node bin/ffmpeg-quick.js progress test.mp4 10 --dry-run
120+
node bin/ffmpeg-quick.js backdrop test.mp4 --dry-run
121+
node bin/ffmpeg-quick.js count-frames test.mp4 --dry-run
114122
115123
- name: Test real encode (compress a generated video)
116124
run: |
117125
ffmpeg -f lavfi -i testsrc=duration=2:size=320x240:rate=15 -c:v libx264 -pix_fmt yuv420p test-input.mp4
126+
ffmpeg -f lavfi -i testsrc=duration=1:size=160x120:rate=5 test-input-gif.gif
118127
node bin/ffmpeg-quick.js compress test-input.mp4 -y
119128
node bin/ffmpeg-quick.js trim test-input.mp4 -s 0 -d 1 -y
120129
node bin/ffmpeg-quick.js resize test-input.mp4 -w 160 -y
@@ -163,3 +172,7 @@ jobs:
163172
node bin/ffmpeg-quick.js pixelate test-input.mp4 -y
164173
node bin/ffmpeg-quick.js timecode test-input.mp4 -y
165174
node bin/ffmpeg-quick.js compare test-input.mp4 test-input.mp4 -y
175+
node bin/ffmpeg-quick.js progress test-input.mp4 2 -y
176+
node bin/ffmpeg-quick.js backdrop test-input.mp4 --size 640x480 -y
177+
node bin/ffmpeg-quick.js gif-to-video test-input-gif.gif -y
178+
node bin/ffmpeg-quick.js count-frames test-input.mp4

bin/ffmpeg-quick.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,21 @@ import { register as pixelate } from "../src/commands/pixelate.js";
8484
import { register as timecode } from "../src/commands/timecode.js";
8585
import { register as compare } from "../src/commands/compare.js";
8686
import { register as concatAudio } from "../src/commands/concat-audio.js";
87+
import { register as audioPitch } from "../src/commands/audio-pitch.js";
88+
import { register as audioEq } from "../src/commands/audio-eq.js";
89+
import { register as panAudio } from "../src/commands/pan-audio.js";
90+
import { register as trimSilence } from "../src/commands/trim-silence.js";
91+
import { register as gifToVideo } from "../src/commands/gif-to-video.js";
92+
import { register as progress } from "../src/commands/progress.js";
93+
import { register as backdrop } from "../src/commands/backdrop.js";
94+
import { register as countFrames } from "../src/commands/count-frames.js";
8795

8896
const program = new Command();
8997

9098
program
9199
.name("ffmpeg-quick")
92100
.description("Quick FFmpeg presets for common video tasks")
93-
.version("2.0.0");
101+
.version("2.1.0");
94102

95103
compress(program);
96104
gif(program);
@@ -175,5 +183,13 @@ pixelate(program);
175183
timecode(program);
176184
compare(program);
177185
concatAudio(program);
186+
audioPitch(program);
187+
audioEq(program);
188+
panAudio(program);
189+
trimSilence(program);
190+
gifToVideo(program);
191+
progress(program);
192+
backdrop(program);
193+
countFrames(program);
178194

179195
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": "2.0.0",
3+
"version": "2.1.0",
44
"description": "Quick FFmpeg presets for common video tasks / FFmpegプリセットCLI",
55
"type": "module",
66
"bin": {

src/commands/audio-eq.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { run } from "../run.js";
2+
import { outputName } from "../utils.js";
3+
4+
export function register(program) {
5+
program
6+
.command("audio-eq")
7+
.description("Apply audio equalizer (bass / treble adjustment)")
8+
.argument("<input>", "Input video/audio file")
9+
.option("--bass <dB>", "Bass gain in dB (-20 to 20)", "0")
10+
.option("--treble <dB>", "Treble gain in dB (-20 to 20)", "0")
11+
.option("--mid <dB>", "Mid-range gain in dB (-20 to 20)", "0")
12+
.option("-o, --output <path>", "Output file path")
13+
.option("--dry-run", "Print the FFmpeg command without running it")
14+
.option("-y", "Overwrite output without asking")
15+
.action((input, opts) => {
16+
const filters = [];
17+
if (opts.bass !== "0") filters.push(`bass=g=${opts.bass}`);
18+
if (opts.treble !== "0") filters.push(`treble=g=${opts.treble}`);
19+
if (opts.mid !== "0") filters.push(`equalizer=f=1000:t=h:width=500:g=${opts.mid}`);
20+
21+
if (filters.length === 0) {
22+
console.error("Error: specify at least --bass, --treble, or --mid.");
23+
process.exit(1);
24+
}
25+
26+
const out = opts.output || outputName(input, "eq");
27+
const args = ["-i", input, "-af", filters.join(","), "-c:v", "copy"];
28+
29+
if (opts.y) args.push("-y");
30+
args.push(out);
31+
run(args, { dryRun: opts.dryRun });
32+
});
33+
}

src/commands/audio-pitch.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { run } from "../run.js";
2+
import { outputName } from "../utils.js";
3+
4+
export function register(program) {
5+
program
6+
.command("audio-pitch")
7+
.description("Change audio pitch without changing speed")
8+
.argument("<input>", "Input video/audio file")
9+
.argument("<semitones>", "Pitch shift in semitones (e.g. 2, -3)")
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, semitones, opts) => {
14+
const n = parseFloat(semitones);
15+
if (isNaN(n)) {
16+
console.error("Error: semitones must be a number (e.g. 2, -3).");
17+
process.exit(1);
18+
}
19+
20+
// rubberband pitch shift: 2^(semitones/12)
21+
const factor = Math.pow(2, n / 12);
22+
const filter = `rubberband=pitch=${factor.toFixed(6)}`;
23+
const suffix = n >= 0 ? `pitch+${n}` : `pitch${n}`;
24+
const out = opts.output || outputName(input, suffix);
25+
const args = ["-i", input, "-af", filter, "-c:v", "copy"];
26+
27+
if (opts.y) args.push("-y");
28+
args.push(out);
29+
run(args, { dryRun: opts.dryRun });
30+
});
31+
}

src/commands/backdrop.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("backdrop")
7+
.description("Add blurred background to vertical video (for 16:9 output)")
8+
.argument("<input>", "Input vertical video file")
9+
.option("--size <WxH>", "Output canvas size", "1920x1080")
10+
.option("--blur <n>", "Background blur strength", "20")
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 match = opts.size.match(/^(\d+)x(\d+)$/);
16+
if (!match) {
17+
console.error("Error: --size must be WxH (e.g. 1920x1080).");
18+
process.exit(1);
19+
}
20+
const [, w, h] = match;
21+
22+
const filter = [
23+
`[0:v]scale=${w}:${h}:force_original_aspect_ratio=increase,crop=${w}:${h},boxblur=${opts.blur}[bg]`,
24+
`[0:v]scale=${w}:${h}:force_original_aspect_ratio=decrease[fg]`,
25+
`[bg][fg]overlay=(W-w)/2:(H-h)/2`,
26+
].join(";");
27+
28+
const out = opts.output || outputName(input, "backdrop");
29+
const args = ["-i", input, "-filter_complex", filter, "-c:a", "copy"];
30+
31+
if (opts.y) args.push("-y");
32+
args.push(out);
33+
run(args, { dryRun: opts.dryRun });
34+
});
35+
}

src/commands/count-frames.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+
3+
export function register(program) {
4+
program
5+
.command("count-frames")
6+
.description("Count total number of frames in a video")
7+
.argument("<input>", "Input video file")
8+
.option("--dry-run", "Print the ffprobe command without running it")
9+
.action((input, opts) => {
10+
const args = [
11+
"-v", "error",
12+
"-count_frames",
13+
"-select_streams", "v:0",
14+
"-show_entries", "stream=nb_read_frames",
15+
"-of", "csv=p=0",
16+
input,
17+
];
18+
19+
run(args, { bin: "ffprobe", dryRun: opts.dryRun });
20+
});
21+
}

src/commands/gif-to-video.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { run } from "../run.js";
2+
import { basename, extname, dirname, join } from "node:path";
3+
4+
export function register(program) {
5+
program
6+
.command("gif-to-video")
7+
.description("Convert GIF to video (MP4)")
8+
.argument("<input>", "Input GIF file")
9+
.option("--loop <n>", "Number of loops (0 = single play)", "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 dir = dirname(input);
15+
const base = basename(input, extname(input));
16+
const out = opts.output || join(dir, `${base}.mp4`);
17+
18+
const args = ["-i", input];
19+
if (parseInt(opts.loop, 10) > 0) {
20+
args.unshift("-stream_loop", opts.loop);
21+
}
22+
args.push(
23+
"-movflags", "faststart",
24+
"-pix_fmt", "yuv420p",
25+
"-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2",
26+
);
27+
28+
if (opts.y) args.push("-y");
29+
args.push(out);
30+
run(args, { dryRun: opts.dryRun });
31+
});
32+
}

src/commands/pan-audio.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { run } from "../run.js";
2+
import { outputName } from "../utils.js";
3+
4+
export function register(program) {
5+
program
6+
.command("pan-audio")
7+
.description("Pan audio left/right (stereo positioning)")
8+
.argument("<input>", "Input video/audio file")
9+
.argument("<position>", "Pan position: -1.0 (full left) to 1.0 (full right), 0 = center")
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, position, opts) => {
14+
const p = parseFloat(position);
15+
if (isNaN(p) || p < -1 || p > 1) {
16+
console.error("Error: position must be between -1.0 (left) and 1.0 (right).");
17+
process.exit(1);
18+
}
19+
20+
// Calculate left/right volumes
21+
const left = Math.min(1, 1 - p).toFixed(2);
22+
const right = Math.min(1, 1 + p).toFixed(2);
23+
const filter = `pan=stereo|c0=${left}*c0+0*c1|c1=0*c0+${right}*c1`;
24+
const out = opts.output || outputName(input, "panned");
25+
const args = ["-i", input, "-af", filter, "-c:v", "copy"];
26+
27+
if (opts.y) args.push("-y");
28+
args.push(out);
29+
run(args, { dryRun: opts.dryRun });
30+
});
31+
}

src/commands/progress.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { run } from "../run.js";
2+
import { outputName } from "../utils.js";
3+
4+
export function register(program) {
5+
program
6+
.command("progress")
7+
.description("Add a progress bar overlay to video")
8+
.argument("<input>", "Input video file")
9+
.argument("<duration>", "Total video duration in seconds")
10+
.option("--pos <position>", "Position: top or bottom", "bottom")
11+
.option("--color <name>", "Progress bar color", "red")
12+
.option("--height <px>", "Bar height in pixels", "5")
13+
.option("-o, --output <path>", "Output file path")
14+
.option("--dry-run", "Print the FFmpeg command without running it")
15+
.option("-y", "Overwrite output without asking")
16+
.action((input, duration, opts) => {
17+
const h = opts.height;
18+
const y = opts.pos === "top" ? "0" : `ih-${h}`;
19+
const filter = `drawbox=x=0:y=${y}:w='iw*t/${duration}':h=${h}:color=${opts.color}:t=fill`;
20+
21+
const out = opts.output || outputName(input, "progress");
22+
const args = ["-i", input, "-vf", filter, "-c:a", "copy"];
23+
24+
if (opts.y) args.push("-y");
25+
args.push(out);
26+
run(args, { dryRun: opts.dryRun });
27+
});
28+
}

0 commit comments

Comments
 (0)