Skip to content

Commit 957c55a

Browse files
omitsu-devclaude
authored andcommitted
feat: add 8 new commands — replace-audio, slideshow, chroma, edge, negative, posterize, sidecar, preview (v1.9.0)
New commands: - replace-audio: Swap video's audio track with another file - slideshow: Create slideshow video from multiple images - chroma: Adjust hue / color temperature - edge: Edge detection filter (sobel, prewitt, roberts) - negative: Invert colors (negative film effect) - posterize: Reduce color levels (poster art effect) - sidecar: Embed subtitle file as selectable track (not burned in) - preview: Generate short highlight preview of a video https://claude.ai/code/session_01BPZRs1uut8bAVcEQsjWjsw
1 parent f689e47 commit 957c55a

11 files changed

Lines changed: 299 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ jobs:
9494
node bin/ffmpeg-quick.js vintage test.mp4 --dry-run
9595
node bin/ffmpeg-quick.js zoom test.mp4 --dry-run
9696
node bin/ffmpeg-quick.js interpolate test.mp4 60 --dry-run
97+
node bin/ffmpeg-quick.js replace-audio test.mp4 test.mp3 --dry-run
98+
node bin/ffmpeg-quick.js slideshow test.jpg test.jpg --dry-run
99+
node bin/ffmpeg-quick.js chroma test.mp4 --hue 45 --dry-run
100+
node bin/ffmpeg-quick.js edge test.mp4 --dry-run
101+
node bin/ffmpeg-quick.js negative test.mp4 --dry-run
102+
node bin/ffmpeg-quick.js posterize test.mp4 --dry-run
103+
node bin/ffmpeg-quick.js sidecar test.mp4 subs.srt --dry-run
104+
node bin/ffmpeg-quick.js preview test.mp4 --dry-run
97105
98106
- name: Test real encode (compress a generated video)
99107
run: |
@@ -138,3 +146,7 @@ jobs:
138146
node bin/ffmpeg-quick.js deflicker test-input.mp4 -y
139147
node bin/ffmpeg-quick.js noise test-input.mp4 -y
140148
node bin/ffmpeg-quick.js vintage test-input.mp4 -y
149+
node bin/ffmpeg-quick.js chroma test-input.mp4 --hue 90 -y
150+
node bin/ffmpeg-quick.js edge test-input.mp4 -y
151+
node bin/ffmpeg-quick.js negative test-input.mp4 -y
152+
node bin/ffmpeg-quick.js posterize test-input.mp4 -y

bin/ffmpeg-quick.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,21 @@ import { register as noise } from "../src/commands/noise.js";
6767
import { register as vintage } from "../src/commands/vintage.js";
6868
import { register as zoom } from "../src/commands/zoom.js";
6969
import { register as interpolate } from "../src/commands/interpolate.js";
70+
import { register as replaceAudio } from "../src/commands/replace-audio.js";
71+
import { register as slideshow } from "../src/commands/slideshow.js";
72+
import { register as chroma } from "../src/commands/chroma.js";
73+
import { register as edge } from "../src/commands/edge.js";
74+
import { register as negative } from "../src/commands/negative.js";
75+
import { register as posterize } from "../src/commands/posterize.js";
76+
import { register as sidecar } from "../src/commands/sidecar.js";
77+
import { register as preview } from "../src/commands/preview.js";
7078

7179
const program = new Command();
7280

7381
program
7482
.name("ffmpeg-quick")
7583
.description("Quick FFmpeg presets for common video tasks")
76-
.version("1.8.0");
84+
.version("1.9.0");
7785

7886
compress(program);
7987
gif(program);
@@ -141,5 +149,13 @@ noise(program);
141149
vintage(program);
142150
zoom(program);
143151
interpolate(program);
152+
replaceAudio(program);
153+
slideshow(program);
154+
chroma(program);
155+
edge(program);
156+
negative(program);
157+
posterize(program);
158+
sidecar(program);
159+
preview(program);
144160

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

src/commands/chroma.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 } from "../utils.js";
3+
4+
export function register(program) {
5+
program
6+
.command("chroma")
7+
.description("Adjust hue / color temperature")
8+
.argument("<input>", "Input video file")
9+
.option("--hue <degrees>", "Hue rotation in degrees (-180 to 180)", "0")
10+
.option("--saturation <n>", "Saturation multiplier (0-3)", "1")
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 filter = `hue=h=${opts.hue}:s=${opts.saturation}`;
16+
const out = opts.output || outputName(input, "chroma");
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/edge.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("edge")
7+
.description("Apply edge detection filter")
8+
.argument("<input>", "Input video file")
9+
.option("--mode <mode>", "Detection mode: sobel, prewitt, roberts", "sobel")
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 modeMap = {
15+
sobel: "edgedetect=mode=colormix:high=0",
16+
prewitt: "prewitt",
17+
roberts: "roberts",
18+
};
19+
20+
const filter = modeMap[opts.mode];
21+
if (!filter) {
22+
console.error("Error: --mode must be sobel, prewitt, or roberts.");
23+
process.exit(1);
24+
}
25+
26+
const out = opts.output || outputName(input, "edge");
27+
const args = ["-i", input, "-vf", filter, "-c:a", "copy"];
28+
29+
if (opts.y) args.push("-y");
30+
args.push(out);
31+
run(args, { dryRun: opts.dryRun });
32+
});
33+
}

src/commands/negative.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("negative")
7+
.description("Invert colors (negative effect)")
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, "negative");
14+
const args = ["-i", input, "-vf", "negate", "-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/posterize.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { run } from "../run.js";
2+
import { outputName } from "../utils.js";
3+
4+
export function register(program) {
5+
program
6+
.command("posterize")
7+
.description("Reduce color levels (posterization effect)")
8+
.argument("<input>", "Input video file")
9+
.option("--levels <n>", "Number of color levels per channel (2-256)", "4")
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, "posterized");
15+
const filter = `format=rgb24,lutrgb=r='val-mod(val,256/${opts.levels})':g='val-mod(val,256/${opts.levels})':b='val-mod(val,256/${opts.levels})'`;
16+
const args = ["-i", input, "-vf", filter, "-c:a", "copy"];
17+
18+
if (opts.y) args.push("-y");
19+
args.push(out);
20+
run(args, { dryRun: opts.dryRun });
21+
});
22+
}

src/commands/preview.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { run } from "../run.js";
2+
import { outputName } from "../utils.js";
3+
4+
export function register(program) {
5+
program
6+
.command("preview")
7+
.description("Generate a short highlight preview of a video")
8+
.argument("<input>", "Input video file")
9+
.option("--clips <n>", "Number of sample clips", "5")
10+
.option("--clip-duration <sec>", "Duration of each clip in seconds", "2")
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 clips = parseInt(opts.clips, 10);
16+
const clipDur = parseFloat(opts.clipDuration);
17+
18+
// Build select filter to pick evenly spaced clips
19+
// Uses the select filter with periodic sampling
20+
const selectParts = [];
21+
for (let i = 0; i < clips; i++) {
22+
// Each clip: between(t, start, start+clipDur)
23+
// We use a fraction-based approach assuming unknown total duration
24+
selectParts.push(`between(t,t_total*${i}/${clips},t_total*${i}/${clips}+${clipDur})`);
25+
}
26+
27+
// Simpler approach: use segment + select with trim
28+
// Most reliable: use the select with periodic segments
29+
const interval = clipDur;
30+
const filter = `select='if(lt(mod(t\\,floor(t/${clips})),${clipDur}),1,0)',setpts=N/FRAME_RATE/TB`;
31+
32+
const out = opts.output || outputName(input, "preview");
33+
const args = [
34+
"-i", input,
35+
"-vf", filter,
36+
"-af", `aselect='if(lt(mod(t\\,floor(t/${clips})),${clipDur}),1,0)',asetpts=N/SR/TB`,
37+
"-t", String(clips * clipDur),
38+
];
39+
40+
if (opts.y) args.push("-y");
41+
args.push(out);
42+
run(args, { dryRun: opts.dryRun });
43+
});
44+
}

src/commands/replace-audio.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { run } from "../run.js";
2+
import { outputName } from "../utils.js";
3+
4+
export function register(program) {
5+
program
6+
.command("replace-audio")
7+
.description("Replace audio track with a different audio file")
8+
.argument("<input>", "Input video file")
9+
.argument("<audio>", "New audio file (mp3, wav, etc.)")
10+
.option("--shortest", "End when the shorter stream ends")
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, audio, opts) => {
15+
const out = opts.output || outputName(input, "newaudio");
16+
const args = [
17+
"-i", input,
18+
"-i", audio,
19+
"-map", "0:v",
20+
"-map", "1:a",
21+
"-c:v", "copy",
22+
];
23+
24+
if (opts.shortest) args.push("-shortest");
25+
if (opts.y) args.push("-y");
26+
args.push(out);
27+
run(args, { dryRun: opts.dryRun });
28+
});
29+
}

src/commands/sidecar.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { run } from "../run.js";
2+
import { outputName } from "../utils.js";
3+
4+
export function register(program) {
5+
program
6+
.command("sidecar")
7+
.description("Embed subtitle file as a selectable track (not burned in)")
8+
.argument("<input>", "Input video file")
9+
.argument("<sub>", "Subtitle file (.srt, .ass, .vtt)")
10+
.option("--language <lang>", "Subtitle language code (e.g. en, ja, es)", "und")
11+
.option("--title <text>", "Subtitle track title")
12+
.option("-o, --output <path>", "Output file path (must be .mkv for multiple subs)")
13+
.option("--dry-run", "Print the FFmpeg command without running it")
14+
.option("-y", "Overwrite output without asking")
15+
.action((input, sub, opts) => {
16+
const out = opts.output || outputName(input, "subtitled", ".mkv");
17+
const args = [
18+
"-i", input,
19+
"-i", sub,
20+
"-c", "copy",
21+
"-c:s", "srt",
22+
"-map", "0:v",
23+
"-map", "0:a",
24+
"-map", "1:0",
25+
"-metadata:s:s:0", `language=${opts.language}`,
26+
];
27+
28+
if (opts.title) {
29+
args.push("-metadata:s:s:0", `title=${opts.title}`);
30+
}
31+
32+
if (opts.y) args.push("-y");
33+
args.push(out);
34+
run(args, { dryRun: opts.dryRun });
35+
});
36+
}

0 commit comments

Comments
 (0)