#include #include #include #include #include #include #include #include #include #include #include struct Subtitle { std::string layer; std::string startTime; std::string endTime; std::string style; std::string name; std::string marginL; std::string marginR; std::string marginV; std::string effect; std::string text; std::string originalLine; bool isDialogue; }; std::size_t findNthCharacter(const std::string &str, char character, int n) { std::size_t pos = -1; for (int i = 0; i < n; ++i) { pos = str.find(character, pos + 1); if (pos == std::string::npos) { return std::string::npos; } } return pos; } /** * @brief 将时间字符串转换为毫秒 * * @param timeStr * @return int */ int timeToMilliseconds(const std::string &timeStr) { int hours, minutes, seconds, hundredths; char colon, dot; std::istringstream iss(timeStr); iss >> hours >> colon >> minutes >> colon >> seconds >> dot >> hundredths; return (hours * 3600 + minutes * 60 + seconds) * 1000 + hundredths * 10; } /** * @brief 将毫秒转换为时间字符串 * * @param ms * @return std::string */ std::string millisecondsToTime(int ms) { if (ms < 0) ms = 0; int totalSeconds = ms / 1000; int hundredths = (ms % 1000) / 10; int hours = totalSeconds / 3600; int minutes = (totalSeconds % 3600) / 60; int seconds = totalSeconds % 60; std::ostringstream oss; oss << hours << ":" << (minutes < 10 ? "0" : "") << minutes << ":" << (seconds < 10 ? "0" : "") << seconds << "." << (hundredths < 10 ? "0" : "") << hundredths; return oss.str(); } /** * @brief 偏移单个字幕的时间 * * @param sub * @param offsetMs * @return Subtitle */ Subtitle shiftSubtitle(const Subtitle &sub, int offsetMs) { if (!sub.isDialogue) { return sub; } Subtitle shifted = sub; // 转换开始时间并偏移 int startMs = timeToMilliseconds(sub.startTime); startMs += offsetMs; shifted.startTime = millisecondsToTime(startMs); // 转换结束时间并偏移 int endMs = timeToMilliseconds(sub.endTime); endMs += offsetMs; shifted.endTime = millisecondsToTime(endMs); // 重新构建行 std::ostringstream oss; oss << "Dialogue: " << shifted.layer << "," << shifted.startTime << "," << shifted.endTime << "," << shifted.style << "," << shifted.name << "," << shifted.marginL << "," << shifted.marginR << "," << shifted.marginV << "," << shifted.effect << "," << shifted.text; shifted.originalLine = oss.str(); return shifted; } Subtitle parseLine(const std::string &line) { constexpr std::string_view dialogue = "Dialogue:"; Subtitle sub; sub.originalLine = line; sub.isDialogue = false; // 检查是否是Dialogue行 if (line.find(dialogue) == 0) { sub.isDialogue = true; std::size_t textStart = findNthCharacter(line, ',', 9); if ((textStart != std::string::npos) && (textStart < line.length())) { sub.text = line.substr(textStart + 1); std::string attr = line.substr(dialogue.length(), textStart - dialogue.length()); boost::algorithm::trim(attr); std::vector splits; boost::algorithm::split(splits, attr, boost::is_any_of(","), boost::algorithm::token_compress_off); sub.layer = splits[0]; sub.startTime = splits[1]; sub.endTime = splits[2]; sub.style = splits[3]; sub.name = splits[4]; sub.marginL = splits[5]; sub.marginR = splits[6]; sub.marginV = splits[7]; sub.effect = splits[8]; } } return sub; } std::string generateTargetFilename(const std::string &filename, const std::string &prefix, const std::string &suffix) { auto start = filename.find(prefix); auto end = filename.find(suffix); return filename.substr(start + prefix.length(), end - (start + prefix.length())); } bool processASSFile(const std::filesystem::path &inputPath, std::filesystem::path &outputPath, int offsetMs) { std::ifstream ifs(inputPath); std::ofstream ofs(outputPath); if (!ifs.is_open()) { std::cerr << "无法打开输入文件: " << inputPath << std::endl; return false; } if (!ofs.is_open()) { std::cerr << "无法创建输出文件: " << outputPath << std::endl; return false; } std::string line; while (std::getline(ifs, line)) { Subtitle sub = parseLine(line); if (sub.isDialogue) { Subtitle shifted = shiftSubtitle(sub, offsetMs); ofs << shifted.originalLine << std::endl; } else { ofs << line << std::endl; } } return true; } class SubtitleShifter { public: void setNameFilter(const std::string &prefix, const std::string &suffix) { if (m_prefix != prefix) { m_prefix = prefix; } if (m_suffix != suffix) { m_suffix = suffix; } } void process(const std::filesystem::path &source, const std::filesystem::path &target, int offsetMs) { if (!std::filesystem::exists(source)) { std::cerr << "错误: 源目录不存在: " << source << std::endl; return; } if (!std::filesystem::exists(target)) { std::cout << "创建目标目录: " << target << std::endl; if (!std::filesystem::create_directories(target)) { std::cerr << "错误: 无法创建目标目录: " << target << std::endl; return; } } std::vector assFiles; try { for (const auto &entry : std::filesystem::directory_iterator(source)) { if (entry.is_regular_file() && entry.path().extension() == ".ass") { assFiles.push_back(entry.path()); } } } catch (const std::filesystem::filesystem_error &ex) { std::cerr << "文件系统错误: " << ex.what() << std::endl; return; } if (assFiles.empty()) { std::cout << "在目录 " << source << " 中未找到.ass文件" << std::endl; return; } std::cout << "找到 " << assFiles.size() << " 个.ass文件" << std::endl; for (const auto &sourceFile : assFiles) { // 生成目标文件名 std::string targetFilename = sourceFile.filename().string(); if (!m_prefix.empty() && !m_suffix.empty()) { targetFilename = generateTargetFilename(sourceFile.filename().string(), m_prefix, m_suffix); } // 确保文件扩展名为.ass if (std::filesystem::path(targetFilename).extension() != ".ass") { targetFilename += ".ass"; } std::filesystem::path targetFile = target / targetFilename; // std::cout << sourceFile << " --> " << targetFile << std::endl; // 处理文件 processASSFile(sourceFile, targetFile, offsetMs); } } private: std::string m_prefix; std::string m_suffix; }; // 1. 找出指定目录下后缀为 `.ass` 下的文件 // 2. 如果目标文件夹不存在,则创建目标文件夹 // 3. 对每个源文件,提取文件名,然后按照模板提出字串,和目标文件夹拼凑成新的目标文件 // ./build/shifter --offset 700 --prefix "[MONSTER][" --suffix "][JP_CN]" -I /mnt/d/Monster int main(int argc, char const *argv[]) { std::string input, output; std::string prefix, suffix; int offset; boost::program_options::options_description desc("Allowed options"); // clang-format off desc.add_options() ("help", "produce help message") ("input,I", boost::program_options::value(&input),"set ass source path") ("output,O", boost::program_options::value(&output)->default_value( "output"),"set ass source path") ("offset", boost::program_options::value(&offset),"ass offset in millseconds") ("prefix", boost::program_options::value(&prefix),"name prefix") ("suffix", boost::program_options::value(&suffix),"name suffix"); // clang-format on boost::program_options::variables_map vm; boost::program_options::store(boost::program_options::parse_command_line(argc, argv, desc), vm); boost::program_options::notify(vm); if (!vm.contains("offset")) { std::cout << desc << std::endl; return 1; } if (std::filesystem::path(output).is_relative()) { std::ostringstream oss; oss << input; if (!input.ends_with('/')) { oss << "/"; } oss << output; output = oss.str(); } std::cout << "input: " << input << std::endl; std::cout << "output: " << output << std::endl; std::cout << "offset: " << offset << std::endl; SubtitleShifter shifter; shifter.setNameFilter(prefix, suffix); shifter.process(input, output, offset); }