Skip to content

Commit b7296d0

Browse files
authored
Feat/custom language support (#12)
* feat: custom language support * chore: log format * feat: i18n * chore: rollback test * chore: bump version * chore: readme en
1 parent 994b0f4 commit b7296d0

12 files changed

Lines changed: 462 additions & 135 deletions

File tree

README.md

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,49 @@
1-
# 一键批量提取流媒体视频的中英字幕,并自动合并为双语字幕。
1+
# dual-subtitle
2+
3+
Extract two embedded subtitle tracks from video files and merge them into a single dual-language subtitle file. Supports batch processing for `.mp4` and `.mkv`.
4+
5+
**[简体中文](README.zh-CN.md)**
6+
27
[![NPM](https://nodei.co/npm/dual-subtitle.png?downloads=true)](https://www.npmjs.com/package/dual-subtitle)
38

4-
## 依赖
5-
需要本地有`Node.js`环境(包括`npm`)
9+
## Requirements
10+
11+
- **Node.js** (with npm): [nodejs.org](https://nodejs.org/)
12+
- The CLI prefers your system **ffmpeg** and **ffprobe**; if missing, it falls back to bundled installers.
13+
14+
## Usage
15+
16+
```bash
17+
# Process all .mp4 and .mkv in the current directory
18+
npx dual-subtitle
19+
20+
# Or specify a directory (with or without trailing /)
21+
npx dual-subtitle /path/to/videos
22+
```
23+
24+
> Without `npx` (e.g. some Synology setups):
25+
> `node /path/to/dual-subtitle/index.js [directory]`
26+
27+
### Output
28+
29+
- Merged subtitle file: `<basename>.<lang1>-<lang2>.srt`
30+
Example: `movie.chi-eng.srt` when auto-detecting Simplified Chinese + English.
631

7-
* 安装【[Node.js/npm环境](https://nodejs.org/zh-cn/)
32+
## Flow
833

9-
## 使用
34+
1. For **each video**, the tool scans embedded subtitle streams.
35+
2. **Before running**, there is a **3-second countdown**:
36+
- **Do nothing**: It auto-selects **Simplified Chinese (chi)** and **English (eng)** and merges them. If either is missing, it lists tracks and asks you to enter the two **stream indices** to merge.
37+
- **Press any key**: Skip auto-detect; it lists all subtitle tracks and asks you to enter the two indices to merge.
38+
3. **Batch**: If you **manually choose indices** for any file in the run, **all following files** in that run also use manual selection (no countdown, no chi/eng auto-detect).
1039

11-
1. 进入视频所在目录
12-
2. 命令行执行:`npx dual-subtitle`
40+
## UI language
1341

14-
注:有些环境(比如群晖)如果没有`npx`,可以用`npm exec`代替。
42+
- **Default**: English. If the system or environment suggests Chinese (e.g. `LANG`, `LC_ALL`, or on macOS the primary system language), the UI switches to Chinese.
43+
- **Override**: Set `DUAL_SUBTITLE_LANG=zh` or `DUAL_SUBTITLE_LANG=en` to force the language.
1544

16-
生成的字幕文件会以`.chs-eng.srt`结尾。
45+
## About
1746

18-
## 介绍
19-
此工具主要是为了满足在Infuse上看流媒体视频时,能够显示双语字幕的需求。Infuse本身并不支持同时显示2条不同语言的字幕。
47+
Useful for players like Infuse that don’t support showing two subtitle tracks at once: merge two embedded tracks (e.g. Chinese + English) into one dual subtitle file.
2048

21-
更多介绍,可以访问知乎文章:https://zhuanlan.zhihu.com/p/1915534266130997832
49+
More background (Chinese): [Zhihu](https://zhuanlan.zhihu.com/p/1915534266130997832)

README.zh-CN.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# dual-subtitle
2+
3+
一键批量提取视频内嵌字幕中的两条轨道,并合并为双语字幕文件。
4+
5+
[![NPM](https://nodei.co/npm/dual-subtitle.png?downloads=true)](https://www.npmjs.com/package/dual-subtitle)
6+
7+
## 依赖
8+
9+
- 需要本地安装 **Node.js**(含 npm):[Node.js 官网](https://nodejs.org/zh-cn/)
10+
- 本工具会优先使用系统自带的 `ffmpeg` / `ffprobe`;若未安装,会通过依赖包自动使用内置版本。
11+
12+
## 使用
13+
14+
```bash
15+
# 在当前目录下处理所有 .mp4 / .mkv
16+
npx dual-subtitle
17+
18+
# 指定目录(末尾可带或不带 /)
19+
npx dual-subtitle /path/to/videos
20+
```
21+
22+
> 若无 `npx`(如部分群晖环境),可用:`node /path/to/dual-subtitle/index.js [目录]`
23+
24+
### 输出文件
25+
26+
- 合并后的字幕文件名为:`<原文件名>.<语言1>-<语言2>.srt`
27+
- 例如自动匹配到简体中文 + 英语时,生成:`movie.chi-eng.srt`
28+
29+
## 运行流程
30+
31+
1. **每个视频文件**会先扫描内嵌字幕轨道。
32+
2. **启动前有 3 秒倒计时**
33+
- 不按键:自动按「简体中文(chi) + 英语(eng)」查找并合成;若缺某一条,会列出字幕并提示输入要合并的两条**索引**
34+
- **按任意键**:跳过自动查找,直接列出所有字幕,由你依次输入两条要合并的字幕索引。
35+
3. **批量处理**:若在**第一个**(或任意一个)文件中进行了「手动选择索引」,则**本轮后续所有文件**都会直接进入手动选择流程,不再倒计时、也不再自动匹配 chi/eng。
36+
37+
## 界面语言
38+
39+
- **默认**:根据系统语言自动选择中文或英文(会读取环境变量 `LANG` / `LC_ALL` 等;在 macOS 上还会读取系统首选语言)。
40+
- **强制指定**
41+
`DUAL_SUBTITLE_LANG=zh``DUAL_SUBTITLE_LANG=en` 可覆盖自动检测。
42+
43+
## 简介
44+
45+
主要用于在 Infuse 等播放器上观看流媒体时,将两条内嵌字幕(如中英)合并成一条双语字幕轨道,以解决播放器无法同时显示两条字幕的问题。
46+
47+
更多说明见知乎文章:<https://zhuanlan.zhihu.com/p/1915534266130997832>

analyzeMedia.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
import { execSync } from 'child_process';
22
import ffprobe from "ffprobe";
33
import ffprobeInstaller from '@ffprobe-installer/ffprobe';
4-
import {config} from "./config.js";
4+
import { config } from "./config.js";
5+
import { t } from './i18n.js';
56

67
const getFFprobePath = () => {
78
try {
89
// 检查系统是否安装了 ffprobe
910
execSync('ffprobe -version', { stdio: 'ignore' });
1011
// 如果能执行到这里,说明系统已安装
11-
console.log('使用本地ffprobe');
12+
console.log(t('usingLocalFfprobe'));
1213
return 'ffprobe'; // 返回系统命令
1314
} catch (e) {
1415
return ffprobeInstaller.path;
1516
}
1617
}
1718

1819
export const analyzeMedia = (file) => {
19-
console.log(`获取字幕信息...`);
20+
console.log(t('gettingSubtitleInfo'));
2021
const ffprobePath = getFFprobePath();
2122

2223
/*
@@ -35,7 +36,8 @@ export const analyzeMedia = (file) => {
3536
duration: Math.round(stream.duration),
3637
frames: Number(stream.tags.NUMBER_OF_FRAMES) || 0
3738
}));
38-
console.log(`找到 ${subTitles.length} 条字幕。`);
39+
// 使用 printf 风格 + 多参数,让编辑器高亮数字
40+
console.log(t('foundSubtitleCount'), subTitles.length);
3941
resolve(subTitles);
4042
})
4143
.catch(function (err) {

config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
const config = {
22
workdir: './',
33
exts: ['.mp4', '.mkv'],
4-
srtTag: 'chs-eng',
54
};
65

76
if (process.argv.length > 2) {

extractSub.js

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ import readline from 'readline';
22
import { execSync } from 'child_process';
33
import ffmpegInstaller from '@ffmpeg-installer/ffmpeg';
44
import ffmpeg from 'fluent-ffmpeg';
5-
import {config} from "./config.js";
6-
import {removeExtension} from "./utils.js";
5+
import { config } from "./config.js";
6+
import { removeExtension } from "./utils.js";
7+
import { t } from './i18n.js';
78

89
const getFFmpegPath = () => {
910
try {
1011
// 检查系统是否安装了 ffprobe
1112
execSync('ffmpeg -version', { stdio: 'ignore' });
1213
// 如果能执行到这里,说明系统已安装
13-
console.log('使用本地ffmpeg');
14+
console.log(t('usingLocalFfmpeg'));
1415
return 'ffmpeg'; // 返回系统命令
1516
} catch (e) {
1617
return ffmpegInstaller.path;
@@ -39,8 +40,11 @@ const timemarkToSeconds = (timemark) => {
3940

4041
export const extractSub = (filename, targetSubs) => {
4142
return new Promise((resolve, reject) => {
42-
const mainSrt = `${removeExtension(filename)}.chs.srt`;
43-
const secondarySrt = `${removeExtension(filename)}.eng.srt`;
43+
// 使用字幕的 code 或 index 来生成文件名
44+
const code1 = targetSubs[0].code || `sub${targetSubs[0].index}`;
45+
const code2 = targetSubs[1].code || `sub${targetSubs[1].index}`;
46+
const mainSrt = `${removeExtension(filename)}.${code1}.srt`;
47+
const secondarySrt = `${removeExtension(filename)}.${code2}.srt`;
4448
const duration = targetSubs[0].duration;
4549
let startTs = 0;
4650

@@ -52,7 +56,7 @@ export const extractSub = (filename, targetSubs) => {
5256
.outputOptions(['-map', `0:${targetSubs[1].index}`, '-c', 'copy'])
5357
.run()
5458
.on('start', function (str) {
55-
console.log('正在提取字幕文件...', str);
59+
console.log(t('extractingStart'), str);
5660
startTs = Date.now();
5761
})
5862
.on('progress', function (progress) {
@@ -61,14 +65,19 @@ export const extractSub = (filename, targetSubs) => {
6165
const elapsedSec = startTs ? (Date.now() - startTs) / 1000 : 0;
6266
const remainingSec = fraction > 0 ? elapsedSec * (1 - fraction) / fraction : 0;
6367
readline.cursorTo(process.stdout, 0);
64-
process.stdout.write(`字幕提取中,进度:${(progressPercent || 0)}% | 预计剩余:${formatSeconds(remainingSec)}`);
68+
process.stdout.write(
69+
t('extractingProgress', {
70+
progressPercent,
71+
remaining: formatSeconds(remainingSec),
72+
}),
73+
);
6574
})
6675
.on('end', function (str) {
67-
console.log('\n字幕提取完成。');
76+
console.log(t('extractingDone'));
6877
resolve([mainSrt, secondarySrt]);
6978
})
7079
.on('error', function (err) {
71-
console.log('字幕提取出错:', err);
80+
console.log(t('extractingError', { err }));
7281
reject(err);
7382
});
7483
});

0 commit comments

Comments
 (0)