Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions extractSub.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import readline from 'readline';
import { execSync } from 'child_process';
import ffmpegInstaller from '@ffmpeg-installer/ffmpeg';
import ffmpeg from 'fluent-ffmpeg';
import {config} from "./config.js";
Expand All @@ -16,6 +17,15 @@ const getFFmpegPath = () => {
}
}

const formatSeconds = (totalSeconds) => {
if (!isFinite(totalSeconds) || totalSeconds < 0) totalSeconds = 0;
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = Math.floor(totalSeconds % 60);
const pad = (n) => String(n).padStart(2, '0');
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
}

/**
* "timemark":"00:06:52.07"
* @param timemark
Expand All @@ -32,6 +42,7 @@ export const extractSub = (filename, targetSubs) => {
const mainSrt = `${removeExtension(filename)}.chs.srt`;
const secondarySrt = `${removeExtension(filename)}.eng.srt`;
const duration = targetSubs[0].duration;
let startTs = 0;

ffmpeg.setFfmpegPath(getFFmpegPath());
ffmpeg(config.workdir + filename)
Expand All @@ -42,11 +53,15 @@ export const extractSub = (filename, targetSubs) => {
.run()
.on('start', function (str) {
console.log('正在提取字幕文件...', str);
startTs = Date.now();
})
.on('progress', function (progress) {
const progressPercent = Math.round((timemarkToSeconds(progress.timemark) / duration) * 100);
const fraction = Math.max(0, Math.min(1, timemarkToSeconds(progress.timemark) / duration));
const progressPercent = Math.round((fraction) * 100);
const elapsedSec = startTs ? (Date.now() - startTs) / 1000 : 0;
const remainingSec = fraction > 0 ? elapsedSec * (1 - fraction) / fraction : 0;
readline.cursorTo(process.stdout, 0);
process.stdout.write(`字幕提取中,进度:${(progressPercent || 0)}%`);
process.stdout.write(`字幕提取中,进度:${(progressPercent || 0)}% | 预计剩余:${formatSeconds(remainingSec)}`);
})
.on('end', function (str) {
console.log('\n字幕提取完成。');
Expand Down
83 changes: 70 additions & 13 deletions findSub.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,82 @@
export const findSub = (subTitles) => {
import readline from 'readline';

export const findSub = async (subTitles) => {
console.log('查找简体和英语字幕...');
const chsSub = findChiSub(subTitles);
const engSub = findEngSub(subTitles);
let chsSub = findChiSub(subTitles);
let engSub = findEngSub(subTitles);

if (chsSub === null || engSub === null) {
if (chsSub === null) {
console.log('没有找到简体中文字幕');
}
if (chsSub) {
console.log('找到简体中文字幕,索引为:', chsSub.index);
} else {
console.log('没有找到简体中文字幕');
}

if (engSub === null) {
console.log('没有找到英语字幕');
if (engSub) {
console.log('找到英语字幕,索引为:', engSub.index);
} else {
console.log('没有找到英语字幕');
}

if (!chsSub || !engSub) {
console.log('所有可用字幕信息如下:');
subTitles.forEach((s) => {
console.log(`索引=${s.index}, code=${s.code}, name="${s.name}", duration=${s.duration}, frames=${s.frames}`);
});

if (!chsSub) {
chsSub = await promptForSubIndex(subTitles, '中文');
}

throw new Error('字幕查找失败,中断执行');
if (!engSub) {
engSub = await promptForSubIndex(subTitles, '英文');
}
}

console.log('找到简体中文字幕,索引为:', chsSub.index);
console.log('找到英语字幕,索引为:', engSub.index);
console.log('最终选择的简体中文字幕索引为:', chsSub.index);
console.log('最终选择的英语字幕索引为:', engSub.index);
console.log('时长:', chsSub.duration);

return [chsSub, engSub];
}
};

const promptForSubIndex = (subTitles, label) => {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

const ask = () => {
rl.question(`请输入${label}字幕的索引(按回车退出): `, (answer) => {
const trimmed = answer.trim();
if (trimmed === '') {
console.log('已退出。');
rl.close();
process.exit(0);
}
const value = Number(trimmed);
if (!Number.isInteger(value)) {
console.log('请输入有效的整数索引。');
ask();
return;
}
const target = subTitles.find((s) => s.index === value);
if (!target) {
console.log('未找到该索引对应的字幕,请重新输入。');
ask();
return;
}
rl.close();
resolve({
index: target.index,
duration: target.duration
});
});
};

ask();
});
};

/**
* 目前看到的数据可能有:
Expand All @@ -29,6 +85,7 @@ export const findSub = (subTitles) => {
* 查找策略是:先找 'chi', 如果数量大于1,则进一步找 "简体"
*/
const findChiSub = (subTitles) => {
// TBD: Consider to include 'chs' later
const chineseSubtitles = subTitles.filter(subTitle => subTitle.code === 'chi');

if (chineseSubtitles.length === 0) {
Expand Down
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const main = async () => {
for (const file of mediaFiles) {
console.log(`正在处理:${file}`);
const subTitles = await analyzeMedia(file);
const targetSubs = findSub(subTitles);
const targetSubs = await findSub(subTitles);
const srts = await extractSub(file, targetSubs);
subtitleMerge(config.workdir + srts[0], config.workdir + srts[1], `${config.workdir}${removeExtension(file)}.${config.srtTag}.srt`);
deleteFile(srts[0]);
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"name": "dual-subtitle",
"version": "0.4.0",
"version": "0.5.0",
"main": "index.js",
"type": "module",
"bin": {
"dual-subtitle": "index.js"
},
"scripts": {
"test": "node index.js ./data/",
"test": "node index.js /Volumes/Download/一战再战[杜比视界版本][中文字幕].2025.2160p.iTunes.WEB-DL.DDP.5.1.Atmos.DV.H.265-DreamHD",
"bumpVersion": "npx bbump"
},
"author": "helloint",
Expand Down