Skip to content

Commit 475c6cc

Browse files
authored
Merge pull request #1 from helloint/feat/include-ffmpeg-dep
Feat/include ffmpeg dep
2 parents 47047cf + f13c299 commit 475c6cc

8 files changed

Lines changed: 470 additions & 107 deletions

File tree

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
# 一键批量提取流媒体视频的中英字幕,并自动合并为双语字幕。
2+
[![NPM](https://nodei.co/npm/dual-subtitle.png?downloads=true)](https://www.npmjs.com/package/dual-subtitle)
23

34
## 依赖
4-
需要本地先安装好`FFmpeg`(包含`ffmpeg`, `ffprobe`这两个命令),以及有`Node.js`环境(包括`npm`)
5+
需要本地有`Node.js`环境(包括`npm`)
56

67
* 安装【[Node.js/npm环境](https://nodejs.org/zh-cn/)
7-
* 安装【[FFmpeg](http://ffmpeg.org/download.html)
8-
9-
计划在下个版本里去掉FFmpeg,改为依赖npm的ffmpeg,简化使用。
108

119
## 使用
1210

analyzeMedia.js

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,17 @@
1-
import { spawn } from 'child_process';
1+
import ffprobe from "ffprobe";
2+
import ffprobeStatic from "ffprobe-static";
23
import {config} from "./config.js";
34

4-
const cmd = 'ffprobe';
5-
6-
const getArgs = (file) => (['-i', file, '-hide_banner', '-select_streams', 's', '-show_entries', 'stream=index,codec_name:stream_tags=language,title', '-of', 'csv=p=0']);
7-
85
export const analyzeMedia = (file) => {
96
return new Promise((resolve, reject) => {
10-
const proc = spawn(cmd, getArgs(config.workdir + file));
11-
12-
let stdoutData = '';
13-
let stderrData = '';
14-
15-
proc.stdout.on('data', function(data) {
16-
stdoutData += data.toString();
17-
});
18-
19-
proc.stderr.setEncoding("utf8");
20-
proc.stderr.on('data', function(data) {
21-
stderrData += data;
22-
});
23-
24-
proc.on('close', function(code) {
25-
// console.error('stderr:', stderrData);
26-
if (code === 0) {
27-
resolve(stdoutData);
28-
} else {
29-
reject(new Error(`Process exited with code ${code}: ${stderrData}`));
30-
}
31-
});
32-
33-
proc.on('error', function(err) {
34-
reject(err);
35-
});
7+
ffprobe(config.workdir + file, { path: ffprobeStatic.path })
8+
.then(function (info) {
9+
// console.log(info);
10+
resolve(info);
11+
})
12+
.catch(function (err) {
13+
// console.error(err);
14+
reject(err);
15+
});
3616
});
3717
};

extractSub.js

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,34 @@
1-
import { spawn } from 'child_process';
1+
import ffmpegInstaller from '@ffmpeg-installer/ffmpeg';
2+
import ffmpeg from 'fluent-ffmpeg';
23
import {config} from "./config.js";
34
import {removeExtension} from "./utils.js";
45

5-
const cmd = 'ffmpeg';
6-
7-
const getArgs = (file, subIdx, main, secondary) => (['-y', '-i', file, '-map', `0:${subIdx[0]}`, '-c', 'copy', main, '-map', `0:${subIdx[1]}`, '-c', 'copy', secondary]);
6+
ffmpeg.setFfmpegPath(ffmpegInstaller.path);
87

98
export const extractSub = (filename, subIdx) => {
109
return new Promise((resolve, reject) => {
1110
const mainSrt = `${removeExtension(filename)}.chs.srt`;
1211
const secondarySrt = `${removeExtension(filename)}.eng.srt`;
13-
const proc = spawn(cmd, getArgs(config.workdir + filename, subIdx, config.workdir + mainSrt, config.workdir + secondarySrt));
14-
15-
let stdoutData = '';
16-
let stderrData = '';
17-
18-
proc.stdout.on('data', function(data) {
19-
stdoutData += data.toString();
20-
});
2112

22-
proc.stderr.setEncoding("utf8");
23-
proc.stderr.on('data', function(data) {
24-
stderrData += data;
25-
});
26-
27-
proc.on('close', function(code) {
28-
// console.error('stderr:', stderrData);
29-
if (code === 0) {
13+
ffmpeg(config.workdir + filename)
14+
.output(config.workdir + mainSrt)
15+
.outputOptions(['-map', `0:${subIdx[0]}`, '-c', 'copy'])
16+
.output(config.workdir + secondarySrt)
17+
.outputOptions(['-map', `0:${subIdx[1]}`, '-c', 'copy'])
18+
.run()
19+
.on('start', function (str) {
20+
console.log('转换任务开始~', str);
21+
})
22+
.on('progress', function (progress) {
23+
console.log(`进行中,完成${(progress.percent || 0)}%`);
24+
})
25+
.on('end', function (str) {
26+
console.log('转换任务完成!');
3027
resolve([mainSrt, secondarySrt]);
31-
} else {
32-
reject(new Error(`Process exited with code ${code}: ${stderrData}`));
33-
}
34-
});
35-
36-
proc.on('error', function(err) {
37-
reject(err);
38-
});
28+
})
29+
.on('error', function (err) {
30+
console.log('转换任务出错:', err);
31+
reject(err);
32+
});
3933
});
4034
};

findIndex.js

Lines changed: 73 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,76 @@
1-
export const findIndex = (text) => {
2-
const lines = text.split('\n');
3-
const result = [];
4-
5-
for (const line of lines) {
6-
const parts = line.split(',');
7-
if (parts.length >= 3) { // 至少要有3部分:id,格式,语言代码
8-
const id = parts[0];
9-
const langCode = parts[2].toLowerCase();
10-
const langName = parts[3] ? parts[3].toLowerCase() : '';
11-
12-
// 检查简体中文:代码为chi且名称包含"简体"或"simplified"
13-
const isSimplifiedChinese = langCode === 'chi' && (
14-
langName.includes('简体') ||
15-
langName.includes('simplified') ||
16-
langName === '简体' // 只有"简体"的情况
17-
);
18-
19-
// 检查英语:代码为eng或名称包含"english"
20-
const isEnglish = langCode === 'eng' ||
21-
langName.includes('english') ||
22-
langName === 'sdh' || // eng,SDH
23-
langName === ''; // 只有eng代码的情况
24-
25-
if (isSimplifiedChinese || isEnglish) {
26-
result.push(parseInt(id));
27-
}
28-
}
1+
export const findIndex = (info) => {
2+
const subTitles = info.streams.filter((stream) => stream.codec_type === 'subtitle').map(stream => ({
3+
index: stream.index,
4+
code: stream.tags.language.toLowerCase(),
5+
name: stream.tags.title ? stream.tags.title.toLowerCase() : ''
6+
}));
7+
8+
const chsIdx = findChiSub(subTitles);
9+
const engIdx = findEngSub(subTitles);
10+
11+
if (chsIdx === undefined) {
12+
console.log('没有找到简体中文字幕');
13+
} else {
14+
console.log('找到简体中文字幕,索引为:', chsIdx);
15+
}
16+
17+
if (engIdx === undefined) {
18+
console.log('没有找到英语中文字幕');
19+
} else {
20+
console.log('找到英语字幕,索引为:', engIdx);
21+
}
22+
23+
if (chsIdx === undefined || engIdx === undefined) {
24+
throw new Error('字幕查找失败,中断执行');
25+
}
26+
27+
return [chsIdx, engIdx];
28+
}
29+
30+
/**
31+
* 目前看到的数据可能有:
32+
* chi,简体
33+
* chi,Simplified Chinese
34+
* 查找策略是:先找 'chi', 如果数量大于1,则进一步找 "简体"
35+
*/
36+
const findChiSub = (subTitles) => {
37+
const chineseSubtitles = subTitles.filter(subTitle => subTitle.code === 'chi');
38+
39+
if (chineseSubtitles.length === 0) {
40+
return undefined;
2941
}
3042

31-
// 按中文在前,英文在后排序
32-
return result.sort((a, b) => {
33-
const lineA = lines.find(l => l.startsWith(a + ','));
34-
const lineB = lines.find(l => l.startsWith(b + ','));
35-
const isChineseA = lineA.includes('chi');
36-
const isChineseB = lineB.includes('chi');
37-
38-
if (isChineseA && !isChineseB) return -1;
39-
if (!isChineseA && isChineseB) return 1;
40-
return a - b;
41-
});
43+
if (chineseSubtitles.length === 1) {
44+
return chineseSubtitles[0].index;
45+
}
46+
47+
// If multiple Chinese subtitles, look for Simplified Chinese
48+
const simplifiedChinese = chineseSubtitles.find(subTitle =>
49+
subTitle.name.includes('简体') ||
50+
subTitle.name.includes('simplified')
51+
);
52+
53+
return simplifiedChinese?.index;
4254
}
55+
56+
/**
57+
* 目前看到的数据可能有:
58+
* 9,subrip,eng
59+
* 10,subrip,eng,SDH
60+
* 23,subrip,eng,English[CC]
61+
* 如果同时有SDH和非SDH版本,选非SDH版本。
62+
*/
63+
const findEngSub = (subTitles) => {
64+
const englishSubs = subTitles.filter(sub => sub.code === 'eng');
65+
66+
if (englishSubs.length === 0) return undefined;
67+
if (englishSubs.length === 1) return englishSubs[0].index;
68+
69+
// Filter out SDH subtitles if there are multiple English options
70+
const nonSDHSubs = englishSubs.filter(sub =>
71+
!sub.name.includes('sdh')
72+
);
73+
74+
// Return first non-SDH sub if available, otherwise first English sub
75+
return (nonSDHSubs[0] || englishSubs[0])?.index;
76+
};

index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ const main = async () => {
1212
const mediaFiles = fs.readdirSync(config.workdir).filter((file) => config.exts.includes(path.extname(file)));
1313

1414
for (const file of mediaFiles) {
15-
console.log(`processing: ${file}`);
16-
const metadata = await analyzeMedia(file);
17-
const subIndex = findIndex(metadata);
15+
console.log(`正在处理:${file}`);
16+
const mediaInfo = await analyzeMedia(file);
17+
const subIndex = findIndex(mediaInfo);
1818
const srts = await extractSub(file, subIndex);
1919
mergeSrtFiles(srts[0], srts[1], `${removeExtension(file)}.${config.srtTag}.srt`);
2020
deleteFile(srts[0]);

mergeSub.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,5 @@ export const mergeSrtFiles = (mainSrtPath, secondarySrtPath, outputPath) => {
8787

8888
// 写入输出文件
8989
fs.writeFileSync(`${config.workdir}${outputPath}`, outputContent.trim());
90+
console.log(`生成双语字幕文件:${outputPath}`);
9091
}

0 commit comments

Comments
 (0)