dsh-ffmpeg 0.4.4 → 0.4.5
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/CHANGELOG.md +21 -0
- package/README.en.md +23 -69
- package/README.md +22 -73
- package/docs/CHANGELOG.en.md +13 -0
- package/docs/USAGE.en.md +79 -0
- package/docs/USAGE.md +83 -0
- package/docs/VALIDATION.md +13 -0
- package/lib/paths.d.ts +2 -2
- package/lib/paths.js +4 -4
- package/lib/tools.d.ts +7 -0
- package/lib/tools.js +29 -23
- package/package.json +5 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 更新记录
|
|
2
|
+
|
|
3
|
+
[返回简介](README.md) · [使用说明](docs/USAGE.md) · [验证记录](docs/VALIDATION.md)
|
|
4
|
+
|
|
5
|
+
[历史英文记录](docs/CHANGELOG.en.md)
|
|
6
|
+
|
|
7
|
+
## 0.4.5 (2026-10-05)
|
|
8
|
+
|
|
9
|
+
- 输入、输出、字幕与抽帧路径按当前 DSH 会话工作区解析,避免宿主启动目录造成找错文件;非法时间参数立即报错。
|
|
10
|
+
|
|
11
|
+
## 0.4.4 (2026-09-28)
|
|
12
|
+
|
|
13
|
+
- 更新官方 Harness 0.2.0-rc.1 的兼容声明和共同加载验证;运行时代码未变。验证范围见[验证记录](docs/VALIDATION.md)。
|
|
14
|
+
|
|
15
|
+
## 0.4.3 (2026-09-19)
|
|
16
|
+
|
|
17
|
+
- 修复抽帧数到旧帧/清单截断、取消被当成功、取消文案丢原因、`+2dB` 被拒;音轨提取非 AAC 自动转 AAC、concat 混合音轨可用、ffprobe 截断明确报错。测试 102 项。
|
|
18
|
+
|
|
19
|
+
## 更早的改动
|
|
20
|
+
|
|
21
|
+
完整历史可查阅 [GitHub 提交记录](https://github.com/STARDUSTLC666/dsh-ffmpeg/commits/master)。
|
package/README.en.md
CHANGED
|
@@ -1,92 +1,46 @@
|
|
|
1
|
-
[中文](README.md)
|
|
2
|
-
|
|
3
|
-
   
|
|
4
|
-
|
|
5
1
|
# dsh-ffmpeg
|
|
6
2
|
|
|
7
|
-
[
|
|
8
|
-
|
|
9
|
-
DSH (DeepSeek Harness) video-processing plugin: seven tools covering probing, cutting, concatenation, transcoding, subtitles, extraction and GIF creation — all powered by ffmpeg/ffprobe.
|
|
3
|
+
[中文](README.md)
|
|
10
4
|
|
|
11
|
-
|
|
5
|
+

|
|
12
6
|
|
|
13
|
-
|
|
7
|
+
Use FFmpeg to process existing audio and video files through natural-language requests.
|
|
14
8
|
|
|
15
|
-
|
|
9
|
+
[](https://www.npmjs.com/package/dsh-ffmpeg) [](https://www.npmjs.com/package/dsh-ffmpeg)
|
|
16
10
|
|
|
17
|
-
##
|
|
11
|
+
## What it does
|
|
18
12
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
- Inspect media and trim, join or transcode video.
|
|
14
|
+
- Add subtitles, create GIFs and extract frames.
|
|
15
|
+
- Execute commands through the host subprocess service.
|
|
22
16
|
|
|
23
|
-
|
|
17
|
+
## Install
|
|
24
18
|
|
|
25
|
-
|
|
19
|
+
In DSH Desktop, install `dsh-ffmpeg` from the Plugins panel. If the bundled dsh command is available:
|
|
26
20
|
|
|
27
21
|
```bash
|
|
28
|
-
dsh plugin --profile
|
|
22
|
+
dsh plugin --profile desktop add dsh-ffmpeg
|
|
29
23
|
```
|
|
30
24
|
|
|
31
|
-
|
|
25
|
+
For the web version, replace `desktop` with `web`. Restart DSH after installation.
|
|
32
26
|
|
|
27
|
+
## Start using it
|
|
33
28
|
|
|
34
|
-
|
|
29
|
+
Provide an input file and ask: “Trim this video to 30 seconds and export a shareable MP4.”
|
|
35
30
|
|
|
36
|
-
|
|
31
|
+
## Requirements and configuration
|
|
37
32
|
|
|
38
|
-
|
|
39
|
-
- id: ffmpeg
|
|
40
|
-
name: 'dsh-ffmpeg'
|
|
41
|
-
config:
|
|
42
|
-
# ffmpegPath: C:\tools\ffmpeg\bin\ffmpeg.exe # explicit path (or DSH_FFMPEG_PATH)
|
|
43
|
-
# ffprobePath: C:\tools\ffmpeg\bin\ffprobe.exe # or DSH_FFPROBE_PATH
|
|
44
|
-
timeoutMs: 300000 # per-operation timeout (default 5 min, 10s - 2h)
|
|
45
|
-
# overwrite: true # allow overwriting outputs (default auto-suffix _1/_2)
|
|
46
|
-
```
|
|
33
|
+
Requires FFmpeg and ffprobe. Executable paths can be configured.
|
|
47
34
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
| Tool | Purpose | Key parameters |
|
|
51
|
-
| :-- | :-- | :-- |
|
|
52
|
-
| `ffmpeg_probe` | Probe media info (format/duration/resolution/fps/bitrate/audio/subtitle streams; multi-video files return a full videos list) | `input` required |
|
|
53
|
-
| `ffmpeg_cut` | Cut a clip (stream copy by default, accurate re-encode optional) | `input` required; `start`/`end`/`duration` |
|
|
54
|
-
| `ffmpeg_concat` | Concatenate 2-20 clips (stream copy for identical codecs / re-encode for mixed; video-only when any input has no audio) | `inputs` array required |
|
|
55
|
-
| `ffmpeg_encode` | Transcode with presets (bilibili 1080p/4K, vertical 1080p, web-720p) plus crf/fps/scale overrides | `input` required; `preset` optional |
|
|
56
|
-
| `ffmpeg_subtitle` | Burn subtitles (SRT/ASS hard subs) | `input` + `subtitle` required |
|
|
57
|
-
| `ffmpeg_extract` | Extract audio (m4a; non-AAC is auto-transcoded to AAC) / frame sequences / single frame / subtitle stream | `input` + `what` required |
|
|
58
|
-
| `ffmpeg_gif` | Video to high-quality GIF (two-pass palette) | `input` required; `fps`/`width`/`duration` optional |
|
|
59
|
-
|
|
60
|
-
### Examples
|
|
61
|
-
|
|
62
|
-
```text
|
|
63
|
-
ffmpeg_probe { input: E:\videos\raw.mp4 }
|
|
64
|
-
ffmpeg_cut { input: E:\videos\raw.mp4, start: 10, end: 30 }
|
|
65
|
-
ffmpeg_encode { input: E:\videos\raw.mp4, preset: bilibili-1080p }
|
|
66
|
-
ffmpeg_subtitle { input: E:\videos\raw.mp4, subtitle: E:\videos\subs.srt }
|
|
67
|
-
ffmpeg_extract { input: E:\videos\raw.webm, what: audio }
|
|
68
|
-
ffmpeg_gif { input: E:\videos\raw.mp4, duration: 3, width: 480 }
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## Safety
|
|
35
|
+
Detailed configuration, tool arguments and troubleshooting are in the [usage guide](docs/USAGE.en.md). For standalone development, follow the Node requirement in [package.json](package.json).
|
|
72
36
|
|
|
73
|
-
|
|
74
|
-
- **Runs on the official DSH subprocess service**: the timeout AbortSignal now really drives tree-scoped termination (SIGTERM → kill; taskkill /T on Windows), zero runtime dependencies
|
|
75
|
-
- **No accidental overwrites**: existing outputs get auto-suffixed; output == input is rejected
|
|
76
|
-
- **Timeout clamps**: per-operation 10s - 2h; probes additionally capped at 60s
|
|
77
|
-
- **Input validation**: time formats, preset enums, crf/fps/scale ranges are validated up front; directory inputs are rejected and extension-less frame-sequence outputs are auto-fixed
|
|
37
|
+
## Documentation
|
|
78
38
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
pnpm test # build + 102 tests, including a real-ffmpeg end-to-end suite (auto-skipped without ffmpeg)
|
|
84
|
-
```
|
|
39
|
+
- [Usage and troubleshooting](docs/USAGE.en.md)
|
|
40
|
+
- [Changelog](CHANGELOG.md)
|
|
41
|
+
- [Validation scope and history](docs/VALIDATION.md)
|
|
42
|
+
- [Report a problem or suggest a feature](https://github.com/STARDUSTLC666/dsh-ffmpeg/issues)
|
|
85
43
|
|
|
86
44
|
## License
|
|
87
45
|
|
|
88
|
-
MIT
|
|
89
|
-
|
|
90
|
-
## Changelog
|
|
91
|
-
|
|
92
|
-
- **0.4.3 (2026-09-18)**: 修复抽帧数到旧帧/清单截断、取消被当成功、取消文案丢原因、`+2dB` 被拒; 音轨提取非 AAC 自动转 AAC、concat 混合音轨可用、ffprobe 截断明确报错. 测试 102 项.
|
|
46
|
+
[MIT](LICENSE)
|
package/README.md
CHANGED
|
@@ -1,97 +1,46 @@
|
|
|
1
|
-
[English](README.en.md)
|
|
2
|
-
|
|
3
1
|
# dsh-ffmpeg
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
   
|
|
3
|
+
[English](README.en.md)
|
|
8
4
|
|
|
9
|
-
|
|
5
|
+

|
|
10
6
|
|
|
7
|
+
用自然语言调用 FFmpeg,处理已有音视频文件。
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
[](https://www.npmjs.com/package/dsh-ffmpeg) [](https://www.npmjs.com/package/dsh-ffmpeg)
|
|
13
10
|
|
|
14
|
-
##
|
|
11
|
+
## 功能
|
|
15
12
|
|
|
16
|
-
|
|
13
|
+
- 探测媒体信息,裁剪、拼接和压制视频。
|
|
14
|
+
- 添加字幕、生成 GIF、抽取画面。
|
|
15
|
+
- 通过宿主 subprocess 执行命令。
|
|
17
16
|
|
|
18
17
|
## 安装
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
dsh plugin --profile web add dsh-ffmpeg
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
需要本机已安装 ffmpeg(`ffmpeg -version` 能出结果即可);不在 PATH 上时可用 `ffmpegPath`/`ffprobePath` 显式指定,或设置环境变量 `DSH_FFMPEG_PATH` / `DSH_FFPROBE_PATH`。
|
|
25
|
-
|
|
26
|
-
## 卸载
|
|
19
|
+
桌面版可在「插件」面板按包名 `dsh-ffmpeg` 安装。已配置 dsh 命令时也可使用:
|
|
27
20
|
|
|
28
21
|
```bash
|
|
29
|
-
dsh plugin --profile
|
|
22
|
+
dsh plugin --profile desktop add dsh-ffmpeg
|
|
30
23
|
```
|
|
31
24
|
|
|
32
|
-
|
|
25
|
+
网页版把命令中的 `desktop` 改为 `web`。安装后重启 DSH。
|
|
33
26
|
|
|
27
|
+
## 开始使用
|
|
34
28
|
|
|
35
|
-
|
|
29
|
+
准备输入文件后可说:“把这段视频裁成 30 秒,并导出适合分享的 MP4。”
|
|
36
30
|
|
|
37
|
-
|
|
31
|
+
## 依赖与配置
|
|
38
32
|
|
|
39
|
-
|
|
40
|
-
- id: ffmpeg
|
|
41
|
-
name: 'dsh-ffmpeg'
|
|
42
|
-
config:
|
|
43
|
-
# ffmpegPath: C:\tools\ffmpeg\bin\ffmpeg.exe # 非 PATH 时显式指定(也可用 DSH_FFMPEG_PATH)
|
|
44
|
-
# ffprobePath: C:\tools\ffmpeg\bin\ffprobe.exe # 也可用 DSH_FFPROBE_PATH
|
|
45
|
-
timeoutMs: 300000 # 单次操作超时(默认 5 分钟,10 秒 - 2 小时)
|
|
46
|
-
# overwrite: true # 允许覆盖同名输出(默认自动加 _1/_2 序号)
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## 工具一览
|
|
50
|
-
|
|
51
|
-
| 工具 | 作用 | 关键参数 |
|
|
52
|
-
| :-- | :-- | :-- |
|
|
53
|
-
| `ffmpeg_probe` | 探测媒体信息(格式/时长/分辨率/帧率/码率/音轨/字幕轨;多视频流时返回完整 videos 列表) | `input` 必填 |
|
|
54
|
-
| `ffmpeg_cut` | 剪辑片段(默认流拷贝秒级,可精确重编码) | `input` 必填;`start`/`end`/`duration` |
|
|
55
|
-
| `ffmpeg_concat` | 拼接 2-20 个片段(同编码流拷贝 / 混合编码重编码;无音轨输入按纯视频拼接) | `inputs` 数组必填 |
|
|
56
|
-
| `ffmpeg_encode` | 转码(B 站 1080p/4K、竖屏 1080p、web-720p 预设 + crf/fps/scale 覆盖) | `input` 必填;`preset` 可选 |
|
|
57
|
-
| `ffmpeg_subtitle` | 字幕烧录(SRT/ASS 硬字幕) | `input`+`subtitle` 必填 |
|
|
58
|
-
| `ffmpeg_extract` | 提取音轨(m4a,非 AAC 自动转 AAC)/ 抽帧序列 / 单帧 / 字幕流 | `input`+`what` 必填 |
|
|
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 可执行文件 | 无 |
|
|
63
|
-
|
|
64
|
-
### 示例
|
|
65
|
-
|
|
66
|
-
```text
|
|
67
|
-
ffmpeg_probe { input: E:\videos\raw.mp4 }
|
|
68
|
-
ffmpeg_cut { input: E:\videos\raw.mp4, start: 10, end: 30 }
|
|
69
|
-
ffmpeg_encode { input: E:\videos\raw.mp4, preset: bilibili-1080p }
|
|
70
|
-
ffmpeg_subtitle { input: E:\videos\raw.mp4, subtitle: E:\videos\subs.srt }
|
|
71
|
-
ffmpeg_extract { input: E:\videos\raw.webm, what: audio }
|
|
72
|
-
ffmpeg_gif { input: E:\videos\raw.mp4, duration: 3, width: 480 }
|
|
73
|
-
ffmpeg_adjust { input: E:\videos\raw.mp4, volume: +2dB }
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## 安全设计
|
|
33
|
+
需要 FFmpeg / ffprobe;可在配置中指定可执行文件路径。
|
|
77
34
|
|
|
78
|
-
|
|
79
|
-
- **进程走 DSH 官方 subprocess 服务**:超时 AbortSignal 会真正触发树级终止(SIGTERM → 强杀,Windows taskkill /T),本插件零运行时依赖
|
|
80
|
-
- **防覆写**:默认不覆盖同名文件(自动加序号),输出与输入相同直接拒绝
|
|
81
|
-
- **超时钳制**:单次操作 10 秒 - 2 小时;探测额外限制 60 秒
|
|
82
|
-
- **参数校验**:时间格式、预设枚举、crf/fps/scale 范围全部前置校验,输入目录、抽帧无扩展名输出等边界也有中文错误/自动修复
|
|
35
|
+
详细配置、工具参数与排错见[使用说明](docs/USAGE.md)。从源码独立开发时,Node 要求以 [package.json](package.json) 为准。
|
|
83
36
|
|
|
84
|
-
##
|
|
37
|
+
## 文档
|
|
85
38
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
39
|
+
- [使用与排错](docs/USAGE.md)
|
|
40
|
+
- [更新记录](CHANGELOG.md)
|
|
41
|
+
- [验证范围与历史记录](docs/VALIDATION.md)
|
|
42
|
+
- [问题反馈与功能建议](https://github.com/STARDUSTLC666/dsh-ffmpeg/issues)
|
|
90
43
|
|
|
91
44
|
## License
|
|
92
45
|
|
|
93
|
-
MIT
|
|
94
|
-
|
|
95
|
-
## 版本记录
|
|
96
|
-
|
|
97
|
-
- **0.4.3(2026-09-18)**:修复抽帧数到旧帧/清单截断、取消被当成功、取消文案丢原因、`+2dB` 被拒;音轨提取非 AAC 自动转 AAC、concat 混合音轨可用、ffprobe 截断明确报错。测试 102 项。
|
|
46
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Historical release notes
|
|
2
|
+
|
|
3
|
+
[Current changelog](../CHANGELOG.md) · [Overview](../README.en.md)
|
|
4
|
+
|
|
5
|
+
These English notes preserve the earlier translations. The main changelog contains the consolidated version history.
|
|
6
|
+
|
|
7
|
+
## 0.4.5 (2026-10-05)
|
|
8
|
+
|
|
9
|
+
- Resolve input, output, subtitles and frame directories against the current DSH session workspace. Reject invalid time arguments before execution.
|
|
10
|
+
|
|
11
|
+
## 0.4.3 (2026-09-19)
|
|
12
|
+
|
|
13
|
+
- 修复抽帧数到旧帧/清单截断、取消被当成功、取消文案丢原因、`+2dB` 被拒; 音轨提取非 AAC 自动转 AAC、concat 混合音轨可用、ffprobe 截断明确报错. 测试 102 项.
|
package/docs/USAGE.en.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# dsh-ffmpeg usage guide
|
|
2
|
+
|
|
3
|
+
[Overview](../README.en.md) · [Changelog](../CHANGELOG.md) · [Validation](VALIDATION.md)
|
|
4
|
+
|
|
5
|
+
## Current improvements
|
|
6
|
+
|
|
7
|
+
Relative paths resolve against the current session workspace. Explicit start / duration values must be valid; invalid values never silently fall back to full-video processing.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
dsh plugin --profile web add dsh-ffmpeg
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
ffmpeg must be installed locally (`ffmpeg -version` should work); use `ffmpegPath` / `ffprobePath`, or the `DSH_FFMPEG_PATH` / `DSH_FFPROBE_PATH` environment variables, when it is not on PATH.
|
|
16
|
+
|
|
17
|
+
## Uninstall
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
dsh plugin --profile web remove dsh-ffmpeg
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then restart the web service. To clean up fully, also remove the plugin entry from your profile `cordis.patch.yml` if you overrode it.
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
Override the plugin row in your profile's `cordis.patch.yml` (defaults apply when absent):
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
- id: ffmpeg
|
|
31
|
+
name: 'dsh-ffmpeg'
|
|
32
|
+
config:
|
|
33
|
+
# ffmpegPath: C:\tools\ffmpeg\bin\ffmpeg.exe # explicit path (or DSH_FFMPEG_PATH)
|
|
34
|
+
# ffprobePath: C:\tools\ffmpeg\bin\ffprobe.exe # or DSH_FFPROBE_PATH
|
|
35
|
+
timeoutMs: 300000 # per-operation timeout (default 5 min, 10s - 2h)
|
|
36
|
+
# overwrite: true # allow overwriting outputs (default auto-suffix _1/_2)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
| Tool | Purpose | Key parameters |
|
|
42
|
+
| :-- | :-- | :-- |
|
|
43
|
+
| `ffmpeg_probe` | Probe media info (format/duration/resolution/fps/bitrate/audio/subtitle streams; multi-video files return a full videos list) | `input` required |
|
|
44
|
+
| `ffmpeg_cut` | Cut a clip (stream copy by default, accurate re-encode optional) | `input` required; `start`/`end`/`duration` |
|
|
45
|
+
| `ffmpeg_concat` | Concatenate 2-20 clips (stream copy for identical codecs / re-encode for mixed; video-only when any input has no audio) | `inputs` array required |
|
|
46
|
+
| `ffmpeg_encode` | Transcode with presets (bilibili 1080p/4K, vertical 1080p, web-720p) plus crf/fps/scale overrides | `input` required; `preset` optional |
|
|
47
|
+
| `ffmpeg_subtitle` | Burn subtitles (SRT/ASS hard subs) | `input` + `subtitle` required |
|
|
48
|
+
| `ffmpeg_extract` | Extract audio (m4a; non-AAC is auto-transcoded to AAC) / frame sequences / single frame / subtitle stream | `input` + `what` required |
|
|
49
|
+
| `ffmpeg_gif` | Video to high-quality GIF (two-pass palette) | `input` required; `fps`/`width`/`duration` optional |
|
|
50
|
+
|
|
51
|
+
### Examples
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
ffmpeg_probe { input: E:\videos\raw.mp4 }
|
|
55
|
+
ffmpeg_cut { input: E:\videos\raw.mp4, start: 10, end: 30 }
|
|
56
|
+
ffmpeg_encode { input: E:\videos\raw.mp4, preset: bilibili-1080p }
|
|
57
|
+
ffmpeg_subtitle { input: E:\videos\raw.mp4, subtitle: E:\videos\subs.srt }
|
|
58
|
+
ffmpeg_extract { input: E:\videos\raw.webm, what: audio }
|
|
59
|
+
ffmpeg_gif { input: E:\videos\raw.mp4, duration: 3, width: 480 }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Safety
|
|
63
|
+
|
|
64
|
+
- **No shell**: every argument is passed as its own argv element — user input cannot inject commands
|
|
65
|
+
- **Runs on the official DSH subprocess service**: the timeout AbortSignal now really drives tree-scoped termination (SIGTERM → kill; taskkill /T on Windows), zero runtime dependencies
|
|
66
|
+
- **No accidental overwrites**: existing outputs get auto-suffixed; output == input is rejected
|
|
67
|
+
- **Timeout clamps**: per-operation 10s - 2h; probes additionally capped at 60s
|
|
68
|
+
- **Input validation**: time formats, preset enums, crf/fps/scale ranges are validated up front; directory inputs are rejected and extension-less frame-sequence outputs are auto-fixed
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pnpm install
|
|
74
|
+
pnpm test # build + 104 tests
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
package/docs/USAGE.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# dsh-ffmpeg 使用说明
|
|
2
|
+
|
|
3
|
+
[返回简介](../README.md) · [更新记录](../CHANGELOG.md) · [验证记录](VALIDATION.md)
|
|
4
|
+
|
|
5
|
+
## 本次改进
|
|
6
|
+
|
|
7
|
+
相对文件路径以当前会话工作区为基准。提供 start / duration 时必须是合法时间,非法输入不会退回默认值或意外处理整段视频。
|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
dsh plugin --profile web add dsh-ffmpeg
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
需要本机已安装 ffmpeg(`ffmpeg -version` 能出结果即可);不在 PATH 上时可用 `ffmpegPath`/`ffprobePath` 显式指定,或设置环境变量 `DSH_FFMPEG_PATH` / `DSH_FFPROBE_PATH`。
|
|
16
|
+
|
|
17
|
+
## 卸载
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
dsh plugin --profile web remove dsh-ffmpeg
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
卸载后重启 Web 服务。如需彻底清理,可再手动删除自己 profile `cordis.patch.yml` 中覆盖的插件行。
|
|
24
|
+
|
|
25
|
+
## 配置
|
|
26
|
+
|
|
27
|
+
在你自己的 profile 的 `cordis.patch.yml` 里覆盖本插件行(缺省时全部用默认值):
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
- id: ffmpeg
|
|
31
|
+
name: 'dsh-ffmpeg'
|
|
32
|
+
config:
|
|
33
|
+
# ffmpegPath: C:\tools\ffmpeg\bin\ffmpeg.exe # 非 PATH 时显式指定(也可用 DSH_FFMPEG_PATH)
|
|
34
|
+
# ffprobePath: C:\tools\ffmpeg\bin\ffprobe.exe # 也可用 DSH_FFPROBE_PATH
|
|
35
|
+
timeoutMs: 300000 # 单次操作超时(默认 5 分钟,10 秒 - 2 小时)
|
|
36
|
+
# overwrite: true # 允许覆盖同名输出(默认自动加 _1/_2 序号)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 工具一览
|
|
40
|
+
|
|
41
|
+
| 工具 | 作用 | 关键参数 |
|
|
42
|
+
| :-- | :-- | :-- |
|
|
43
|
+
| `ffmpeg_probe` | 探测媒体信息(格式/时长/分辨率/帧率/码率/音轨/字幕轨;多视频流时返回完整 videos 列表) | `input` 必填 |
|
|
44
|
+
| `ffmpeg_cut` | 剪辑片段(默认流拷贝秒级,可精确重编码) | `input` 必填;`start`/`end`/`duration` |
|
|
45
|
+
| `ffmpeg_concat` | 拼接 2-20 个片段(同编码流拷贝 / 混合编码重编码;无音轨输入按纯视频拼接) | `inputs` 数组必填 |
|
|
46
|
+
| `ffmpeg_encode` | 转码(B 站 1080p/4K、竖屏 1080p、web-720p 预设 + crf/fps/scale 覆盖) | `input` 必填;`preset` 可选 |
|
|
47
|
+
| `ffmpeg_subtitle` | 字幕烧录(SRT/ASS 硬字幕) | `input`+`subtitle` 必填 |
|
|
48
|
+
| `ffmpeg_extract` | 提取音轨(m4a,非 AAC 自动转 AAC)/ 抽帧序列 / 单帧 / 字幕流 | `input`+`what` 必填 |
|
|
49
|
+
| `ffmpeg_gif` | 视频转高质量 GIF(两遍调色板) | `input` 必填;`fps`/`width`/`duration` 可选 |
|
|
50
|
+
| `ffmpeg_frames` | 批量抽帧给视觉模型读图(定间隔或指定时间点,PNG/JPG) | `input` 必填;`every`/`times` 二选一 |
|
|
51
|
+
| `ffmpeg_adjust` | 变速(音视频同步)/ 音量 / 静音 / 旋转(竖横屏互转),可组合 | `input` 必填;`speed`/`volume`/`mute`/`rotate` 至少一个 |
|
|
52
|
+
| `ffmpeg_health` | 自检 ffmpeg / ffprobe 可执行文件 | 无 |
|
|
53
|
+
|
|
54
|
+
### 示例
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
ffmpeg_probe { input: E:\videos\raw.mp4 }
|
|
58
|
+
ffmpeg_cut { input: E:\videos\raw.mp4, start: 10, end: 30 }
|
|
59
|
+
ffmpeg_encode { input: E:\videos\raw.mp4, preset: bilibili-1080p }
|
|
60
|
+
ffmpeg_subtitle { input: E:\videos\raw.mp4, subtitle: E:\videos\subs.srt }
|
|
61
|
+
ffmpeg_extract { input: E:\videos\raw.webm, what: audio }
|
|
62
|
+
ffmpeg_gif { input: E:\videos\raw.mp4, duration: 3, width: 480 }
|
|
63
|
+
ffmpeg_adjust { input: E:\videos\raw.mp4, volume: +2dB }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 安全设计
|
|
67
|
+
|
|
68
|
+
- **无 shell**:所有参数以独立 argv 数组传递,用户输入不可能注入命令
|
|
69
|
+
- **进程走 DSH 官方 subprocess 服务**:超时 AbortSignal 会真正触发树级终止(SIGTERM → 强杀,Windows taskkill /T),本插件零运行时依赖
|
|
70
|
+
- **防覆写**:默认不覆盖同名文件(自动加序号),输出与输入相同直接拒绝
|
|
71
|
+
- **超时钳制**:单次操作 10 秒 - 2 小时;探测额外限制 60 秒
|
|
72
|
+
- **参数校验**:时间格式、预设枚举、crf/fps/scale 范围全部前置校验,输入目录、抽帧无扩展名输出等边界也有中文错误/自动修复
|
|
73
|
+
|
|
74
|
+
## 开发
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pnpm install
|
|
78
|
+
pnpm test # 构建 + 104 个测试
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# dsh-ffmpeg 验证记录
|
|
2
|
+
|
|
3
|
+
本页整理原 README 的历史验证说明,保留当时的版本、日期与范围。自动测试、启动检查、浏览器操作和真实服务验收分别记录,不能相互替代。更详细的版本验收文件仍保留在仓库中。
|
|
4
|
+
|
|
5
|
+
## 原中文记录
|
|
6
|
+
|
|
7
|
+
验证宿主:官方源码构建的 Harness `0.2.0-rc.1`(commit `407e65c8`)+ Node `24.16.0`(2026-09-28)。96 项插件测试在隔离环境全部通过;同一个宿主里 18 个插件共同加载,注册 10 个工具,工具 schema 与健康检查契约通过。本轮未启用真实端口与外部服务。
|
|
8
|
+
|
|
9
|
+
## Original English record
|
|
10
|
+
|
|
11
|
+
Validation host: Harness `0.2.0-rc.1` built from official sources (commit `407e65c8`) with Node `24.16.0` on 2026-09-28. All 96 plugin tests pass in an isolated environment; all 18 plugins mount together in one host registering 10 tools, with tool schemas and health-check contracts passing. No live ports or external services were exercised in this round.
|
|
12
|
+
|
|
13
|
+
2026-09-13 fix: retain the service receiver when calling `subprocess.spawn`, preventing failures caused by passing the method as an unbound callback. Verified against a real isolated host subprocess service. FFmpeg/ffprobe version checks and probing a real MP4 pass. This fixes the tool invocation failure reported in [#3](https://github.com/STARDUSTLC666/dsh-ffmpeg/issues/3).
|
package/lib/paths.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** 校验输入文件存在且是文件;返回绝对路径。 */
|
|
2
|
-
export declare function assertInputFile(input: string): string;
|
|
2
|
+
export declare function assertInputFile(input: string, cwd?: string): string;
|
|
3
3
|
/** 清洗文件名中的危险字符。 */
|
|
4
4
|
export declare function sanitizeName(name: string): string;
|
|
5
5
|
/**
|
|
6
6
|
* 决定输出路径:缺省时放在输入同目录,名字 = 输入名 + suffix + ext。
|
|
7
7
|
* 目标已存在且不允许覆写时自动追加 _1/_2…。
|
|
8
8
|
*/
|
|
9
|
-
export declare function resolveOutputPath(input: string, explicit: string | undefined, suffix: string, ext: string, overwrite: boolean): string;
|
|
9
|
+
export declare function resolveOutputPath(input: string, explicit: string | undefined, suffix: string, ext: string, overwrite: boolean, cwd?: string): string;
|
package/lib/paths.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
import { existsSync, statSync } from 'node:fs';
|
|
7
7
|
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
8
8
|
/** 校验输入文件存在且是文件;返回绝对路径。 */
|
|
9
|
-
export function assertInputFile(input) {
|
|
10
|
-
const absolute = resolve(input);
|
|
9
|
+
export function assertInputFile(input, cwd = process.cwd()) {
|
|
10
|
+
const absolute = resolve(cwd, input);
|
|
11
11
|
if (!existsSync(absolute)) {
|
|
12
12
|
throw new Error('输入文件不存在:' + input);
|
|
13
13
|
}
|
|
@@ -25,10 +25,10 @@ export function sanitizeName(name) {
|
|
|
25
25
|
* 决定输出路径:缺省时放在输入同目录,名字 = 输入名 + suffix + ext。
|
|
26
26
|
* 目标已存在且不允许覆写时自动追加 _1/_2…。
|
|
27
27
|
*/
|
|
28
|
-
export function resolveOutputPath(input, explicit, suffix, ext, overwrite) {
|
|
28
|
+
export function resolveOutputPath(input, explicit, suffix, ext, overwrite, cwd = process.cwd()) {
|
|
29
29
|
let target;
|
|
30
30
|
if (explicit !== undefined && explicit.trim() !== '') {
|
|
31
|
-
target = resolve(explicit.trim());
|
|
31
|
+
target = resolve(cwd, explicit.trim());
|
|
32
32
|
}
|
|
33
33
|
else {
|
|
34
34
|
const base = basename(input, extname(input));
|
package/lib/tools.d.ts
CHANGED
|
@@ -15,6 +15,13 @@ export interface ContentBlock {
|
|
|
15
15
|
/** v0.1.2-rc.1 工具执行上下文中本插件需要的公共最小面。 */
|
|
16
16
|
export interface FfmpegToolRunContext {
|
|
17
17
|
readonly signal: AbortSignal;
|
|
18
|
+
readonly agent?: {
|
|
19
|
+
session?: {
|
|
20
|
+
header?: {
|
|
21
|
+
cwd?: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
};
|
|
18
25
|
}
|
|
19
26
|
/** 注册给 ctx.tools.register 的原始工具定义。 */
|
|
20
27
|
export interface FfmpegToolDefinition {
|
package/lib/tools.js
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
import { mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { randomUUID } from 'node:crypto';
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
|
-
import { basename, dirname, extname, join } from 'node:path';
|
|
10
|
+
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
11
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';
|
|
15
|
+
function executionCwd(exec) { return exec?.agent?.session?.header?.cwd || process.cwd(); }
|
|
15
16
|
/** 编译作者 DSL 为原始 JSON Schema(正是 defineTool 存为 definition.parameters 的值)。 */
|
|
16
17
|
function compileParameters(spec) {
|
|
17
18
|
const properties = {};
|
|
@@ -55,8 +56,12 @@ function requiredTime(args, key, label) {
|
|
|
55
56
|
return value;
|
|
56
57
|
}
|
|
57
58
|
function optionalTime(args, key) {
|
|
59
|
+
if (args[key] === undefined)
|
|
60
|
+
return undefined;
|
|
58
61
|
const value = parseTimeArg(args[key]);
|
|
59
|
-
|
|
62
|
+
if (value === null)
|
|
63
|
+
throw new Error(key + ' 非法:请用非负秒数或 HH:MM:SS[.mmm] 格式。');
|
|
64
|
+
return value;
|
|
60
65
|
}
|
|
61
66
|
function stringArray(args, key) {
|
|
62
67
|
const value = args[key];
|
|
@@ -188,7 +193,7 @@ export function buildFfmpegTools(config, runner) {
|
|
|
188
193
|
},
|
|
189
194
|
async execute(rawArgs, exec) {
|
|
190
195
|
const args = asRecord(rawArgs);
|
|
191
|
-
const input = assertInputFile(requiredString(args, 'input', '输入文件'));
|
|
196
|
+
const input = assertInputFile(requiredString(args, 'input', '输入文件'), executionCwd(exec));
|
|
192
197
|
const result = await runChecked(runner, probeArgs(cfg.ffprobePath, input), Math.min(timeout, 60000), 'ffprobe', exec?.signal);
|
|
193
198
|
const media = parseProbeJson(result.stdout);
|
|
194
199
|
return { ok: true, input, summary: buildProbeSummary(media), ...media };
|
|
@@ -215,7 +220,7 @@ export function buildFfmpegTools(config, runner) {
|
|
|
215
220
|
},
|
|
216
221
|
async execute(rawArgs, exec) {
|
|
217
222
|
const args = asRecord(rawArgs);
|
|
218
|
-
const input = assertInputFile(requiredString(args, 'input', '输入文件'));
|
|
223
|
+
const input = assertInputFile(requiredString(args, 'input', '输入文件'), executionCwd(exec));
|
|
219
224
|
const start = optionalTime(args, 'start') ?? 0;
|
|
220
225
|
const end = optionalTime(args, 'end');
|
|
221
226
|
let duration;
|
|
@@ -230,7 +235,7 @@ export function buildFfmpegTools(config, runner) {
|
|
|
230
235
|
throw new Error('duration 必须大于 0。');
|
|
231
236
|
}
|
|
232
237
|
const reencode = args.reencode === true;
|
|
233
|
-
const output = resolveOutputPath(input, optionalString(args, 'output'), '.cut', extname(input) || '.mp4', cfg.overwrite);
|
|
238
|
+
const output = resolveOutputPath(input, optionalString(args, 'output'), '.cut', extname(input) || '.mp4', cfg.overwrite, executionCwd(exec));
|
|
234
239
|
await runChecked(runner, cutArgs(cfg.ffmpegPath, { input, start, duration, output, overwrite: cfg.overwrite, reencode }), timeout, 'ffmpeg 剪辑', exec?.signal);
|
|
235
240
|
return { output, start, duration, reencode };
|
|
236
241
|
},
|
|
@@ -258,10 +263,10 @@ export function buildFfmpegTools(config, runner) {
|
|
|
258
263
|
throw new Error('inputs 至少需要 2 个文件(当前 ' + inputs.length + ' 个)。');
|
|
259
264
|
if (inputs.length > 20)
|
|
260
265
|
throw new Error('inputs 最多 20 个文件(当前 ' + inputs.length + ' 个)。');
|
|
261
|
-
const absolute = inputs.map(assertInputFile);
|
|
266
|
+
const absolute = inputs.map(input => assertInputFile(input, executionCwd(exec)));
|
|
262
267
|
const reencode = args.reencode === true;
|
|
263
268
|
const firstExt = extname(absolute[0]) || '.mp4';
|
|
264
|
-
const output = resolveOutputPath(absolute[0], optionalString(args, 'output'), '.concat', firstExt, cfg.overwrite);
|
|
269
|
+
const output = resolveOutputPath(absolute[0], optionalString(args, 'output'), '.concat', firstExt, cfg.overwrite, executionCwd(exec));
|
|
265
270
|
if (!reencode) {
|
|
266
271
|
const listPath = join(tmpdir(), 'dsh-ffmpeg-concat-' + Date.now() + '-' + randomUUID().slice(0, 8) + '.txt');
|
|
267
272
|
writeFileSync(listPath, concatListContent(absolute), 'utf8');
|
|
@@ -308,7 +313,7 @@ export function buildFfmpegTools(config, runner) {
|
|
|
308
313
|
},
|
|
309
314
|
async execute(rawArgs, exec) {
|
|
310
315
|
const args = asRecord(rawArgs);
|
|
311
|
-
const input = assertInputFile(requiredString(args, 'input', '输入文件'));
|
|
316
|
+
const input = assertInputFile(requiredString(args, 'input', '输入文件'), executionCwd(exec));
|
|
312
317
|
const presetRaw = optionalString(args, 'preset') ?? 'bilibili-1080p';
|
|
313
318
|
if (!ENCODE_PRESETS.includes(presetRaw)) {
|
|
314
319
|
throw new Error('preset 必须是 ' + ENCODE_PRESETS.join(' / ') + ' 之一(当前:' + presetRaw + ')。');
|
|
@@ -335,7 +340,7 @@ export function buildFfmpegTools(config, runner) {
|
|
|
335
340
|
throw new Error('scale 格式必须是 宽:高,如 1920:1080 或 -2:720。');
|
|
336
341
|
scale = scaleRaw;
|
|
337
342
|
}
|
|
338
|
-
const output = resolveOutputPath(input, optionalString(args, 'output'), '.encoded', extname(input) || '.mp4', cfg.overwrite);
|
|
343
|
+
const output = resolveOutputPath(input, optionalString(args, 'output'), '.encoded', extname(input) || '.mp4', cfg.overwrite, executionCwd(exec));
|
|
339
344
|
await runChecked(runner, encodeArgs(cfg.ffmpegPath, { input, output, preset, crf, fps, scale, overwrite: cfg.overwrite }), timeout, 'ffmpeg 转码', exec?.signal);
|
|
340
345
|
return { output, preset, crf: crf ?? 'preset', fps: fps ?? null, scale: scale ?? null };
|
|
341
346
|
},
|
|
@@ -358,9 +363,9 @@ export function buildFfmpegTools(config, runner) {
|
|
|
358
363
|
},
|
|
359
364
|
async execute(rawArgs, exec) {
|
|
360
365
|
const args = asRecord(rawArgs);
|
|
361
|
-
const input = assertInputFile(requiredString(args, 'input', '输入视频'));
|
|
362
|
-
const subtitlePath = assertInputFile(requiredString(args, 'subtitle', '字幕文件'));
|
|
363
|
-
const output = resolveOutputPath(input, optionalString(args, 'output'), '.sub', extname(input) || '.mp4', cfg.overwrite);
|
|
366
|
+
const input = assertInputFile(requiredString(args, 'input', '输入视频'), executionCwd(exec));
|
|
367
|
+
const subtitlePath = assertInputFile(requiredString(args, 'subtitle', '字幕文件'), executionCwd(exec));
|
|
368
|
+
const output = resolveOutputPath(input, optionalString(args, 'output'), '.sub', extname(input) || '.mp4', cfg.overwrite, executionCwd(exec));
|
|
364
369
|
await runChecked(runner, subtitleArgs(cfg.ffmpegPath, { input, subtitle: subtitlePath, output, overwrite: cfg.overwrite }), timeout, 'ffmpeg 字幕', exec?.signal);
|
|
365
370
|
return { output, mode: 'burn' };
|
|
366
371
|
},
|
|
@@ -387,7 +392,7 @@ export function buildFfmpegTools(config, runner) {
|
|
|
387
392
|
},
|
|
388
393
|
async execute(rawArgs, exec) {
|
|
389
394
|
const args = asRecord(rawArgs);
|
|
390
|
-
const input = assertInputFile(requiredString(args, 'input', '输入文件'));
|
|
395
|
+
const input = assertInputFile(requiredString(args, 'input', '输入文件'), executionCwd(exec));
|
|
391
396
|
const what = requiredString(args, 'what', '提取内容');
|
|
392
397
|
const allowed = ['audio', 'frames', 'frame', 'subtitle'];
|
|
393
398
|
if (!allowed.includes(what))
|
|
@@ -398,16 +403,17 @@ export function buildFfmpegTools(config, runner) {
|
|
|
398
403
|
const streamIndex = typeof args.streamIndex === 'number' && Number.isInteger(args.streamIndex) && args.streamIndex >= 0 ? args.streamIndex : 0;
|
|
399
404
|
let output;
|
|
400
405
|
if (what === 'audio') {
|
|
401
|
-
output = resolveOutputPath(input, optionalString(args, 'output'), '.audio', '.m4a', cfg.overwrite);
|
|
406
|
+
output = resolveOutputPath(input, optionalString(args, 'output'), '.audio', '.m4a', cfg.overwrite, executionCwd(exec));
|
|
402
407
|
}
|
|
403
408
|
else if (what === 'frame') {
|
|
404
|
-
output = resolveOutputPath(input, optionalString(args, 'output'), '.frame', '.png', cfg.overwrite);
|
|
409
|
+
output = resolveOutputPath(input, optionalString(args, 'output'), '.frame', '.png', cfg.overwrite, executionCwd(exec));
|
|
405
410
|
}
|
|
406
411
|
else if (what === 'subtitle') {
|
|
407
|
-
output = resolveOutputPath(input, optionalString(args, 'output'), '.subtitle', '.srt', cfg.overwrite);
|
|
412
|
+
output = resolveOutputPath(input, optionalString(args, 'output'), '.subtitle', '.srt', cfg.overwrite, executionCwd(exec));
|
|
408
413
|
}
|
|
409
414
|
else {
|
|
410
|
-
const
|
|
415
|
+
const rawOutput = optionalString(args, 'output');
|
|
416
|
+
const explicit = rawOutput === undefined ? undefined : resolve(executionCwd(exec), rawOutput);
|
|
411
417
|
if (explicit !== undefined) {
|
|
412
418
|
if (explicit.includes('%')) {
|
|
413
419
|
output = explicit;
|
|
@@ -463,7 +469,7 @@ export function buildFfmpegTools(config, runner) {
|
|
|
463
469
|
},
|
|
464
470
|
async execute(rawArgs, exec) {
|
|
465
471
|
const args = asRecord(rawArgs);
|
|
466
|
-
const input = assertInputFile(requiredString(args, 'input', '输入视频'));
|
|
472
|
+
const input = assertInputFile(requiredString(args, 'input', '输入视频'), executionCwd(exec));
|
|
467
473
|
const start = optionalTime(args, 'start') ?? 0;
|
|
468
474
|
const duration = optionalTime(args, 'duration') ?? 10;
|
|
469
475
|
if (duration <= 0)
|
|
@@ -472,7 +478,7 @@ export function buildFfmpegTools(config, runner) {
|
|
|
472
478
|
const fps = typeof fpsRaw === 'number' && Number.isInteger(fpsRaw) ? Math.min(30, Math.max(1, fpsRaw)) : 10;
|
|
473
479
|
const widthRaw = args.width;
|
|
474
480
|
const width = typeof widthRaw === 'number' && Number.isInteger(widthRaw) ? Math.min(1280, Math.max(64, widthRaw)) : 480;
|
|
475
|
-
const output = resolveOutputPath(input, optionalString(args, 'output'), '.gif', '.gif', cfg.overwrite);
|
|
481
|
+
const output = resolveOutputPath(input, optionalString(args, 'output'), '.gif', '.gif', cfg.overwrite, executionCwd(exec));
|
|
476
482
|
// 调色板属于内部临时产物,不能借用用户输出旁的可预测路径;否则 -y 与
|
|
477
483
|
// finally 清理都可能覆盖/删除用户原有的 <output>.palette.png。
|
|
478
484
|
const paletteDir = mkdtempSync(join(tmpdir(), 'dsh-ffmpeg-gif-'));
|
|
@@ -509,13 +515,13 @@ export function buildFfmpegTools(config, runner) {
|
|
|
509
515
|
},
|
|
510
516
|
async execute(rawArgs, exec) {
|
|
511
517
|
const args = asRecord(rawArgs);
|
|
512
|
-
const input = assertInputFile(requiredString(args, 'input', '输入文件'));
|
|
518
|
+
const input = assertInputFile(requiredString(args, 'input', '输入文件'), executionCwd(exec));
|
|
513
519
|
const formatRaw = optionalString(args, 'format')?.toLowerCase() ?? 'png';
|
|
514
520
|
if (formatRaw !== 'png' && formatRaw !== 'jpg' && formatRaw !== 'jpeg')
|
|
515
521
|
throw new Error('format 只支持 png 或 jpg。');
|
|
516
522
|
const ext = formatRaw === 'png' ? '.png' : '.jpg';
|
|
517
523
|
const times = stringArray(args, 'times');
|
|
518
|
-
const outDir = optionalString(args, 'outputDir') ?? join(dirname(input), sanitizeName(basename(input, extname(input))) + '-frames');
|
|
524
|
+
const outDir = resolve(executionCwd(exec), optionalString(args, 'outputDir') ?? join(dirname(input), sanitizeName(basename(input, extname(input))) + '-frames'));
|
|
519
525
|
mkdirSync(outDir, { recursive: true });
|
|
520
526
|
// 每次运行独占 run-* 子目录:历史 frame-* 不参与计数,也不会被本次 -y 覆盖
|
|
521
527
|
const runDir = mkdtempSync(join(outDir, 'run-'));
|
|
@@ -568,7 +574,7 @@ export function buildFfmpegTools(config, runner) {
|
|
|
568
574
|
},
|
|
569
575
|
async execute(rawArgs, exec) {
|
|
570
576
|
const args = asRecord(rawArgs);
|
|
571
|
-
const input = assertInputFile(requiredString(args, 'input', '输入文件'));
|
|
577
|
+
const input = assertInputFile(requiredString(args, 'input', '输入文件'), executionCwd(exec));
|
|
572
578
|
const speed = optionalNumber(args, 'speed');
|
|
573
579
|
if (speed !== undefined && (speed < 0.1 || speed > 100))
|
|
574
580
|
throw new Error('speed 必须在 0.1-100 之间(当前:' + speed + ')。');
|
|
@@ -590,7 +596,7 @@ export function buildFfmpegTools(config, runner) {
|
|
|
590
596
|
// 先探测:确认有没有音轨,避免对无声文件构建音频滤镜报错
|
|
591
597
|
const probeResult = await runChecked(runner, probeArgs(cfg.ffprobePath, input), Math.min(timeout, 60000), 'ffprobe', exec?.signal);
|
|
592
598
|
const hasAudio = parseProbeJson(probeResult.stdout).audio.length > 0;
|
|
593
|
-
const output = resolveOutputPath(input, optionalString(args, 'output'), '.adjust', extname(input) || '.mp4', cfg.overwrite);
|
|
599
|
+
const output = resolveOutputPath(input, optionalString(args, 'output'), '.adjust', extname(input) || '.mp4', cfg.overwrite, executionCwd(exec));
|
|
594
600
|
await runChecked(runner, adjustArgs(cfg.ffmpegPath, { input, output, overwrite: cfg.overwrite, speed, volume, mute, rotate, hasAudio }), timeout, 'ffmpeg 调整', exec?.signal);
|
|
595
601
|
const ops = [];
|
|
596
602
|
if (speed !== undefined)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ffmpeg",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.5",
|
|
4
|
+
"description": "用自然语言调用 FFmpeg,处理已有音视频文件。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"lib",
|
|
18
18
|
"cordis.patch.yml",
|
|
19
19
|
"README.md",
|
|
20
|
-
"README.en.md"
|
|
20
|
+
"README.en.md",
|
|
21
|
+
"docs",
|
|
22
|
+
"CHANGELOG.md"
|
|
21
23
|
],
|
|
22
24
|
"scripts": {
|
|
23
25
|
"build": "tsc -p tsconfig.json",
|