dsh-ffmpeg 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -1,5 +1,7 @@
1
1
  [中文](README.md)
2
2
 
3
+ ![npm](https://img.shields.io/npm/v/dsh-ffmpeg) ![downloads](https://img.shields.io/npm/dm/dsh-ffmpeg) ![license](https://img.shields.io/github/license/STARDUSTLC666/dsh-ffmpeg) ![stars](https://img.shields.io/github/stars/STARDUSTLC666/dsh-ffmpeg?style=social)
4
+
3
5
  # dsh-ffmpeg
4
6
 
5
7
  [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
@@ -8,7 +10,7 @@ DSH (DeepSeek Harness) video-processing plugin: seven tools covering probing, cu
8
10
 
9
11
  ## Compatibility
10
12
 
11
- Verified against `@deepseek-ai/dsh@0.1.1-rc.2` on 2026-08-26. Built for the cordis patch-bundle plugin model (`cordis.patch.yml` + `dsh.bundle.patch`). No runtime imports of `@deepseek-ai/*` internals.
13
+ Verified against `@deepseek-ai/dsh@0.1.2-alpha.2` on 2026-08-31. Built for the cordis patch-bundle plugin model (`cordis.patch.yml` + `dsh.bundle.patch`). No runtime imports of `@deepseek-ai/*` internals.
12
14
 
13
15
  ## Installation
14
16
 
@@ -80,4 +82,4 @@ pnpm test # build + 57 tests, including a real-ffmpeg end-to-end suite (au
80
82
 
81
83
  ## License
82
84
 
83
- MIT
85
+ MIT
package/README.md CHANGED
@@ -2,18 +2,18 @@
2
2
 
3
3
  # dsh-ffmpeg
4
4
 
5
- > **你的 agent 会剪视频了**:探测/剪辑/拼接/转码/字幕/抽帧/GIF,七个工具一条命令。
5
+ > **你的 agent 会剪视频了**:探测/剪辑/拼接/转码/字幕/抽帧/GIF/变速旋转调音,十个工具一条命令。
6
6
 
7
7
  ![npm version](https://img.shields.io/npm/v/dsh-ffmpeg?label=npm&color=blue) ![npm downloads](https://img.shields.io/npm/dm/dsh-ffmpeg) ![license](https://img.shields.io/npm/l/dsh-ffmpeg) ![stars](https://img.shields.io/github/stars/STARDUSTLC666/dsh-ffmpeg?style=social)
8
8
 
9
9
  [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
10
10
 
11
11
 
12
- DSH(DeepSeek Harness)视频处理工具插件:七个工具覆盖探测、剪辑、拼接、转码、字幕、提取与 GIF 制作,全部由 ffmpeg/ffprobe 完成。
12
+ DSH(DeepSeek Harness)视频处理工具插件:十个工具:探测、剪辑、拼接、转码、字幕、提取、GIF、抽帧、调整(变速/音量/静音/旋转)与自检,全部由 ffmpeg/ffprobe 完成。
13
13
 
14
14
  ## 兼容性
15
15
 
16
- 在 `@deepseek-ai/dsh@0.1.1-rc.2` 上验证(2026-08-26)。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。
16
+ 在 `@deepseek-ai/dsh@0.1.2-alpha.2` 上验证(2026-08-31)。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。
17
17
 
18
18
  ## 安装
19
19
 
@@ -57,6 +57,9 @@ dsh plugin --profile web remove dsh-ffmpeg
57
57
  | `ffmpeg_subtitle` | 字幕烧录(SRT/ASS 硬字幕) | `input`+`subtitle` 必填 |
58
58
  | `ffmpeg_extract` | 提取音轨(m4a)/ 抽帧序列 / 单帧 / 字幕流 | `input`+`what` 必填 |
59
59
  | `ffmpeg_gif` | 视频转高质量 GIF(两遍调色板) | `input` 必填;`fps`/`width`/`duration` 可选 |
60
+ | `ffmpeg_frames` | 批量抽帧给视觉模型读图(定间隔或指定时间点,PNG/JPG) | `input` 必填;`every`/`times` 二选一 |
61
+ | `ffmpeg_adjust` | 变速(音视频同步)/ 音量 / 静音 / 旋转(竖横屏互转),可组合 | `input` 必填;`speed`/`volume`/`mute`/`rotate` 至少一个 |
62
+ | `ffmpeg_health` | 自检 ffmpeg / ffprobe 可执行文件 | 无 |
60
63
 
61
64
  ### 示例
62
65
 
package/lib/args.d.ts CHANGED
@@ -87,3 +87,29 @@ export interface GifSpec {
87
87
  export declare function gifPaletteArgs(ffmpeg: string, spec: GifSpec): string[];
88
88
  /** GIF 第二遍:paletteuse 合成。 */
89
89
  export declare function gifUseArgs(ffmpeg: string, spec: GifSpec): string[];
90
+ export type RotateDeg = 90 | 180 | 270;
91
+ export interface AdjustSpec {
92
+ input: string;
93
+ output: string;
94
+ overwrite: boolean;
95
+ /** 倍速:>1 加速,<1 减速(0.1-100)。 */
96
+ speed?: number;
97
+ /** 音量:倍数(如 1.5 / 0.6)或分贝(如 -3dB)。 */
98
+ volume?: string;
99
+ mute?: boolean;
100
+ rotate?: RotateDeg;
101
+ /** 输入是否含音频流(probe 结果),决定是否构建音频滤镜。 */
102
+ hasAudio: boolean;
103
+ }
104
+ /**
105
+ * atempo 单滤镜只接受约 [0.5, 100],超出范围用多个 atempo 级联
106
+ * (0.25 → atempo=0.5,atempo=0.5)。
107
+ */
108
+ export declare function atempoChain(speed: number): string;
109
+ /** 顺时针旋转滤镜:90/270 用 transpose,180 用双翻转。 */
110
+ export declare function rotateFilter(deg: RotateDeg): string;
111
+ /**
112
+ * 调整:变速(视频 setpts + 音频 atempo)、音量、静音、旋转。
113
+ * 只静音/调音量(无变速旋转)时视频走流拷贝;动了画面就重编码。
114
+ */
115
+ export declare function adjustArgs(ffmpeg: string, spec: AdjustSpec): string[];
package/lib/args.js CHANGED
@@ -111,3 +111,69 @@ export function gifUseArgs(ffmpeg, spec) {
111
111
  const filter = 'fps=' + spec.fps + ',scale=' + spec.width + ':-1:flags=lanczos[x];[x][1:v]paletteuse';
112
112
  return [ffmpeg, overwriteFlag(spec.overwrite), '-i', spec.input, '-ss', fmtSeconds(spec.start), '-t', fmtSeconds(spec.duration), '-i', spec.palettePath, '-filter_complex', filter, spec.output];
113
113
  }
114
+ /**
115
+ * atempo 单滤镜只接受约 [0.5, 100],超出范围用多个 atempo 级联
116
+ * (0.25 → atempo=0.5,atempo=0.5)。
117
+ */
118
+ export function atempoChain(speed) {
119
+ const parts = [];
120
+ let remaining = speed;
121
+ while (remaining < 0.5) {
122
+ parts.push('atempo=0.5');
123
+ remaining /= 0.5;
124
+ }
125
+ while (remaining > 100) {
126
+ parts.push('atempo=100');
127
+ remaining /= 100;
128
+ }
129
+ parts.push('atempo=' + Number(remaining.toFixed(4)));
130
+ return parts.join(',');
131
+ }
132
+ /** 顺时针旋转滤镜:90/270 用 transpose,180 用双翻转。 */
133
+ export function rotateFilter(deg) {
134
+ if (deg === 90)
135
+ return 'transpose=1';
136
+ if (deg === 270)
137
+ return 'transpose=2';
138
+ return 'hflip,vflip';
139
+ }
140
+ /**
141
+ * 调整:变速(视频 setpts + 音频 atempo)、音量、静音、旋转。
142
+ * 只静音/调音量(无变速旋转)时视频走流拷贝;动了画面就重编码。
143
+ */
144
+ export function adjustArgs(ffmpeg, spec) {
145
+ const parts = [ffmpeg, overwriteFlag(spec.overwrite), '-i', spec.input];
146
+ const vf = [];
147
+ const af = [];
148
+ const audioUsable = spec.hasAudio && spec.mute !== true;
149
+ if (spec.speed !== undefined) {
150
+ vf.push('setpts=PTS/' + spec.speed);
151
+ if (audioUsable)
152
+ af.push(atempoChain(spec.speed));
153
+ }
154
+ if (spec.volume !== undefined && audioUsable)
155
+ af.push('volume=' + spec.volume);
156
+ if (spec.rotate !== undefined)
157
+ vf.push(rotateFilter(spec.rotate));
158
+ if (vf.length > 0)
159
+ parts.push('-vf', vf.join(','));
160
+ if (af.length > 0)
161
+ parts.push('-af', af.join(','));
162
+ if (vf.length > 0) {
163
+ parts.push('-c:v', 'libx264', '-preset', 'veryfast', '-crf', '18', '-pix_fmt', 'yuv420p');
164
+ }
165
+ else {
166
+ parts.push('-c:v', 'copy');
167
+ }
168
+ if (spec.mute === true || !spec.hasAudio) {
169
+ parts.push('-an');
170
+ }
171
+ else if (af.length > 0) {
172
+ parts.push('-c:a', 'aac', '-b:a', '192k');
173
+ }
174
+ else {
175
+ parts.push('-c:a', 'copy');
176
+ }
177
+ parts.push('-movflags', '+faststart', spec.output);
178
+ return parts;
179
+ }
package/lib/tools.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 八个面向模型的视频工具:probe / cut / concat / encode / subtitle / extract / gif / frames。
2
+ * 九个面向模型的视频工具:probe / cut / concat / encode / subtitle / extract / gif / frames / adjust
3
3
  * 直接调用 ctx.tools.register 注册【编译好的 JSON Schema】参数与 canonical 输出。
4
4
  *
5
5
  * @module dsh-ffmpeg/tools
package/lib/tools.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 八个面向模型的视频工具:probe / cut / concat / encode / subtitle / extract / gif / frames。
2
+ * 九个面向模型的视频工具:probe / cut / concat / encode / subtitle / extract / gif / frames / adjust
3
3
  * 直接调用 ctx.tools.register 注册【编译好的 JSON Schema】参数与 canonical 输出。
4
4
  *
5
5
  * @module dsh-ffmpeg/tools
@@ -8,7 +8,7 @@ import { mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
8
8
  import { randomUUID } from 'node:crypto';
9
9
  import { tmpdir } from 'node:os';
10
10
  import { basename, dirname, extname, join } from 'node:path';
11
- import { concatArgs, concatListContent, cutArgs, ENCODE_PRESETS, encodeArgs, extractArgs, fmtSeconds, frameAtArgs, gifPaletteArgs, gifUseArgs, probeArgs, subtitleArgs, } from './args.js';
11
+ import { adjustArgs, concatArgs, concatListContent, cutArgs, ENCODE_PRESETS, encodeArgs, extractArgs, fmtSeconds, frameAtArgs, gifPaletteArgs, gifUseArgs, probeArgs, subtitleArgs, } from './args.js';
12
12
  import { parseProbeJson } from './ffprobe.js';
13
13
  import { assertInputFile, resolveOutputPath, sanitizeName } from './paths.js';
14
14
  import { parseTimeArg } from './config.js';
@@ -80,12 +80,12 @@ function buildTextRenderer(lines) {
80
80
  const baseSchema = { type: 'object', additionalProperties: true };
81
81
  const videoStreamSchema = {
82
82
  type: 'object',
83
- properties: { width: { type: 'number' }, height: { type: 'number' }, fps: { type: 'number' }, codec: { type: 'string' }, durationSeconds: { type: 'number' }, bitrate: { type: 'number' } },
83
+ properties: { width: { type: 'number' }, height: { type: 'number' }, fps: { oneOf: [{ type: 'number' }, { type: 'null' }] }, codec: { type: 'string' }, durationSeconds: { oneOf: [{ type: 'number' }, { type: 'null' }] }, bitrate: { oneOf: [{ type: 'number' }, { type: 'null' }] } },
84
84
  additionalProperties: true,
85
85
  };
86
86
  const audioStreamSchema = {
87
87
  type: 'object',
88
- properties: { codec: { type: 'string' }, sampleRate: { type: 'number' }, channels: { type: 'number' }, durationSeconds: { type: 'number' } },
88
+ properties: { codec: { type: 'string' }, sampleRate: { oneOf: [{ type: 'number' }, { type: 'null' }] }, channels: { oneOf: [{ type: 'number' }, { type: 'null' }] }, durationSeconds: { oneOf: [{ type: 'number' }, { type: 'null' }] } },
89
89
  additionalProperties: true,
90
90
  };
91
91
  const subtitleStreamSchema = {
@@ -100,9 +100,9 @@ const probeSchema = {
100
100
  input: { type: 'string' },
101
101
  summary: { type: 'string' },
102
102
  formatName: { type: 'string' },
103
- durationSeconds: { type: 'number' },
104
- sizeBytes: { type: 'number' },
105
- bitrate: { type: 'number' },
103
+ durationSeconds: { oneOf: [{ type: 'number' }, { type: 'null' }] },
104
+ sizeBytes: { oneOf: [{ type: 'number' }, { type: 'null' }] },
105
+ bitrate: { oneOf: [{ type: 'number' }, { type: 'null' }] },
106
106
  video: videoStreamSchema,
107
107
  videos: { type: 'array', items: videoStreamSchema },
108
108
  audio: { type: 'array', items: audioStreamSchema },
@@ -507,6 +507,64 @@ export function buildFfmpegTools(config, runner) {
507
507
  },
508
508
  timeoutMs: timeout,
509
509
  };
510
+ const adjust = {
511
+ name: 'ffmpeg_adjust',
512
+ description: '调整媒体:变速(speed 倍率,>1 加速 <1 减速,音视频同步变速)、音量(volume 倍数如 1.5 或分贝如 -3dB)、静音(mute)、顺时针旋转(rotate 90/180/270,竖横屏互转)。可组合使用。只静音/调音量时视频走流拷贝(快);变速/旋转会重编码。四个操作至少给一个。',
513
+ parameters: compileParameters({
514
+ input: { type: 'string', required: true, description: '输入文件(必填)。' },
515
+ speed: { type: 'number', description: '倍速 0.1-100:2 为两倍速,0.5 为半速(可选)。' },
516
+ volume: { type: 'string', description: '音量:倍数如 1.5 / 0.6,或分贝如 -3dB / +2dB(可选)。' },
517
+ mute: { type: 'boolean', description: 'true 时移除音轨(可选)。' },
518
+ rotate: { type: 'integer', description: '顺时针旋转角度:90 / 180 / 270(可选)。' },
519
+ output: { type: 'string', description: '输出路径(可选,默认输入同目录加 .adjust 后缀)。' },
520
+ }),
521
+ output: {
522
+ schema: produceSchema,
523
+ render: buildTextRenderer((_args, value) => {
524
+ const rec = asRecord(value);
525
+ const ops = Array.isArray(rec.ops) ? rec.ops.join(',') : '';
526
+ return ['调整完成:' + rec.output + '(' + ops + ')'];
527
+ }),
528
+ },
529
+ async execute(rawArgs) {
530
+ const args = asRecord(rawArgs);
531
+ const input = assertInputFile(requiredString(args, 'input', '输入文件'));
532
+ const speed = optionalNumber(args, 'speed');
533
+ if (speed !== undefined && (speed < 0.1 || speed > 100))
534
+ throw new Error('speed 必须在 0.1-100 之间(当前:' + speed + ')。');
535
+ const volume = optionalString(args, 'volume');
536
+ if (volume !== undefined && !/^\d+(\.\d+)?$|^-?\d+(\.\d+)?dB$/.test(volume)) {
537
+ throw new Error('volume 格式必须是倍数(如 1.5)或分贝(如 -3dB)。');
538
+ }
539
+ const mute = args.mute === true;
540
+ let rotate;
541
+ const rotateRaw = args.rotate;
542
+ if (rotateRaw !== undefined) {
543
+ if (rotateRaw !== 90 && rotateRaw !== 180 && rotateRaw !== 270)
544
+ throw new Error('rotate 只支持 90 / 180 / 270。');
545
+ rotate = rotateRaw;
546
+ }
547
+ if (speed === undefined && volume === undefined && !mute && rotate === undefined) {
548
+ throw new Error('speed / volume / mute / rotate 至少提供一个。');
549
+ }
550
+ // 先探测:确认有没有音轨,避免对无声文件构建音频滤镜报错
551
+ const probeResult = await runChecked(runner, probeArgs(cfg.ffprobePath, input), Math.min(timeout, 60000), 'ffprobe');
552
+ const hasAudio = parseProbeJson(probeResult.stdout).audio.length > 0;
553
+ const output = resolveOutputPath(input, optionalString(args, 'output'), '.adjust', extname(input) || '.mp4', cfg.overwrite);
554
+ await runChecked(runner, adjustArgs(cfg.ffmpegPath, { input, output, overwrite: cfg.overwrite, speed, volume, mute, rotate, hasAudio }), timeout, 'ffmpeg 调整');
555
+ const ops = [];
556
+ if (speed !== undefined)
557
+ ops.push('倍速 x' + speed);
558
+ if (volume !== undefined)
559
+ ops.push('音量 ' + volume);
560
+ if (mute)
561
+ ops.push('静音');
562
+ if (rotate !== undefined)
563
+ ops.push('旋转 ' + rotate + '°');
564
+ return { output, ops, hasAudio };
565
+ },
566
+ timeoutMs: timeout,
567
+ };
510
568
  const health = {
511
569
  name: 'ffmpeg_health',
512
570
  description: 'dsh-ffmpeg 自检:验证 ffmpeg / ffprobe 可执行文件是否可用(执行 -version)。遇到问题时先运行本工具定位。',
@@ -548,5 +606,5 @@ export function buildFfmpegTools(config, runner) {
548
606
  },
549
607
  timeoutMs: 30000,
550
608
  };
551
- return [probe, cut, concat, encode, subtitle, extract, gif, frames, health];
609
+ return [probe, cut, concat, encode, subtitle, extract, gif, frames, adjust, health];
552
610
  }
package/package.json CHANGED
@@ -1,60 +1,60 @@
1
- {
2
- "name": "dsh-ffmpeg",
3
- "version": "0.3.0",
4
- "description": "DSH 视频处理工具插件:ffmpeg_probe/cut/concat/encode/subtitle/extract/gif 七工具,进程走官方 subprocess 服务,超时树级终止、环境变量路径回退与多视频流探测,零运行时依赖(ffmpeg 本体除外)。",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "types": "lib/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./lib/index.d.ts",
11
- "default": "./lib/index.js"
12
- },
13
- "./cordis.patch.yml": "./cordis.patch.yml",
14
- "./package.json": "./package.json"
15
- },
16
- "files": [
17
- "lib",
18
- "cordis.patch.yml",
19
- "README.md",
20
- "README.en.md"
21
- ],
22
- "scripts": {
23
- "build": "tsc -p tsconfig.json",
24
- "typecheck": "tsc -p tsconfig.json --noEmit",
25
- "test": "pnpm run build && node --test \"test/*.test.mjs\"",
26
- "prepublishOnly": "pnpm run build"
27
- },
28
- "dsh": {
29
- "bundle": {
30
- "patch": "./cordis.patch.yml"
31
- }
32
- },
33
- "keywords": [
34
- "dsh",
35
- "deepseek-harness",
36
- "plugin",
37
- "ffmpeg",
38
- "video",
39
- "media",
40
- "dsh-plugin"
41
- ],
42
- "license": "MIT",
43
- "engines": {
44
- "node": ">=22"
45
- },
46
- "devDependencies": {
47
- "@types/node": "^24.0.0",
48
- "typescript": "^5.6.0"
49
- },
50
- "repository": {
51
- "type": "git",
52
- "url": "git+https://github.com/STARDUSTLC666/dsh-ffmpeg.git"
53
- },
54
- "bugs": {
55
- "url": "https://github.com/STARDUSTLC666/dsh-ffmpeg/issues"
56
- },
57
- "homepage": "https://github.com/STARDUSTLC666/dsh-ffmpeg#readme",
58
- "author": "stardustlc",
59
- "packageManager": "pnpm@11.7.0"
60
- }
1
+ {
2
+ "name": "dsh-ffmpeg",
3
+ "version": "0.4.0",
4
+ "description": "DSH 视频处理工具插件:ffmpeg_probe/cut/concat/encode/subtitle/extract/gif 七工具,进程走官方 subprocess 服务,超时树级终止、环境变量路径回退与多视频流探测,零运行时依赖(ffmpeg 本体除外)。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./cordis.patch.yml": "./cordis.patch.yml",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "lib",
18
+ "cordis.patch.yml",
19
+ "README.md",
20
+ "README.en.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "typecheck": "tsc -p tsconfig.json --noEmit",
25
+ "test": "pnpm run build && node --test \"test/*.test.mjs\"",
26
+ "prepublishOnly": "pnpm run build"
27
+ },
28
+ "dsh": {
29
+ "bundle": {
30
+ "patch": "./cordis.patch.yml"
31
+ }
32
+ },
33
+ "keywords": [
34
+ "dsh",
35
+ "deepseek-harness",
36
+ "plugin",
37
+ "ffmpeg",
38
+ "video",
39
+ "media",
40
+ "dsh-plugin"
41
+ ],
42
+ "license": "MIT",
43
+ "engines": {
44
+ "node": ">=22"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^24.0.0",
48
+ "typescript": "^5.6.0"
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/STARDUSTLC666/dsh-ffmpeg.git"
53
+ },
54
+ "bugs": {
55
+ "url": "https://github.com/STARDUSTLC666/dsh-ffmpeg/issues"
56
+ },
57
+ "homepage": "https://github.com/STARDUSTLC666/dsh-ffmpeg#readme",
58
+ "author": "stardustlc",
59
+ "packageManager": "pnpm@11.7.0"
60
+ }