【代码】JS实现FCPXML文件转换为SRT文件

前言

JS实现.fcpxml文件转换为.srt文件

下载依赖

1
npm install xml2js

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
const fs = require("fs");
const xml2js = require("xml2js");

/**
* 将秒数转换为 SRT 时间格式
* @param {number} totalSeconds - 总秒数
* @returns {string} SRT时间格式
*/
function formatSecondsToSrtTime(totalSeconds) {
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = Math.floor(totalSeconds % 60);
const milliseconds = Math.floor((totalSeconds - Math.floor(totalSeconds)) * 1000);

// 补零到两位/三位
const pad2 = (num) => num.toString().padStart(2, "0");
const pad3 = (num) => num.toString().padStart(3, "0");

return `${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)},${pad3(milliseconds)}`;
}

/**
* 将 FCPXML 时间格式转换为 SRT 时间格式
* @param {string} timeStr - FCPXML时间字符串(如 "5s", "10.5s", "1/24s")
* @param {number} frameRate - 帧率(默认24)
* @returns {string} SRT时间格式(如 "00:00:05,000")
*/
function convertFcpxmlTimeToSrt(timeStr, frameRate = 24) {
// 处理纯秒数(如 "5s", "10.5s")
if (timeStr.endsWith("s") && !timeStr.includes("/")) {
const seconds = parseFloat(timeStr.replace("s", ""));
return formatSecondsToSrtTime(seconds);
}

// 处理帧单位(如 "1/24s" 代表1帧)
if (timeStr.includes("/")) {
const [frames, frameRateStr] = timeStr.replace("s", "").split("/");
const actualFrameRate = parseInt(frameRateStr) || frameRate;
const seconds = parseInt(frames) / actualFrameRate;
return formatSecondsToSrtTime(seconds);
}

// 默认返回0秒
return "00:00:00,000";
}

/**
* 从 FCPXML 文件提取字幕并转换为 SRT
* @param {string} fcpxmlPath - FCPXML文件路径
* @param {string} srtOutputPath - 输出SRT文件路径
* @param {number} frameRate - 视频帧率(默认24)
*/
function fcpxmlToSrt(fcpxmlPath, srtOutputPath, frameRate = 24) {
// 读取FCPXML文件
const xmlContent = fs.readFileSync(fcpxmlPath, "utf8");

// 解析XML
const parser = new xml2js.Parser({explicitArray: true, mergeAttrs: true});
let result;
parser.parseString(xmlContent, (err, parsedResult) => {
if (err) throw err;
result = parsedResult;
});

// 提取所有字幕节点
const subtitles = [];
let subtitleIndex = 1;

// 递归查找所有title/text节点
function findSubtitles(node) {
if (!node) return;

// 处理title节点(FCPXML中的字幕)
if (node.title) {
node.title.forEach(title => {
// 提取字幕核心信息
const offset = title.offset ? title.offset[0] : "0s"; // 开始时间
const duration = title.duration ? title.duration[0] : "0s"; // 持续时间
const text = title.text ? title.text[0] : ""; // 字幕文本

if (text) {
// 计算开始/结束时间
const startSeconds = parseFloat(offset.replace("s", ""));
const durationSeconds = parseFloat(duration.replace("s", ""));
const endSeconds = startSeconds + durationSeconds;

// 转换为SRT时间格式
const startTime = convertFcpxmlTimeToSrt(offset, frameRate);
const endTime = convertFcpxmlTimeToSrt(`${endSeconds}s`, frameRate);

subtitles.push({
index: subtitleIndex++,
startTime,
endTime,
text
});
}
});
}

// 递归遍历子节点
for (const key in node) {
if (typeof node[key] === "object" && node[key] !== null) {
findSubtitles(node[key]);
}
}
}

// 开始查找字幕
findSubtitles(result.fcpxml);

// 生成SRT内容
let srtContent = "";
subtitles.forEach(sub => {
srtContent += `${sub.index}\n`;
srtContent += `${sub.startTime} --> ${sub.endTime}\n`;
srtContent += `${sub.text}\n\n`;
});

// 写入SRT文件
fs.writeFileSync(srtOutputPath, srtContent, "utf8");
console.log(`提取字幕条数: ${subtitles.length}`);
}

// 转换
const fcpxmlPath = "./input.fcpxml";
const srtOutputPath = "./output.srt";
const frameRate = 24; // 视频帧率
fcpxmlToSrt(fcpxmlPath, srtOutputPath, frameRate);

完成