11
This commit is contained in:
@@ -264,6 +264,42 @@ function parseTranscriptionText(trans) {
|
|||||||
return fullText;
|
return fullText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @returns {Array<{ text: string, start: number, end: number }>} start/end 毫秒 */
|
||||||
|
function normalizeAsrTimeToMs(value) {
|
||||||
|
const n = Number(value);
|
||||||
|
if (!Number.isFinite(n) || n < 0) return 0;
|
||||||
|
// 对齐 Electron generateAssSubtitle:>100 视为毫秒,否则视为秒
|
||||||
|
if (n > 100) return Math.round(n);
|
||||||
|
return Math.round(n * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAsrRecord(record) {
|
||||||
|
const text = String(record?.text || record?.sentence || "").trim();
|
||||||
|
if (!text) return null;
|
||||||
|
let start = normalizeAsrTimeToMs(record.start ?? record.begin ?? record.beginTime ?? 0);
|
||||||
|
let end = normalizeAsrTimeToMs(record.end ?? record.endTime ?? start + 2000);
|
||||||
|
if (end <= start) end = start + 2000;
|
||||||
|
return { text, start, end };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {Array<{ text: string, start: number, end: number }>} */
|
||||||
|
function normalizeAsrRecords(records) {
|
||||||
|
if (!Array.isArray(records)) return [];
|
||||||
|
const normalized = records.map(normalizeAsrRecord).filter(Boolean);
|
||||||
|
const MIN_GAP_MS = 50;
|
||||||
|
for (let i = 0; i < normalized.length - 1; i++) {
|
||||||
|
const current = normalized[i];
|
||||||
|
const next = normalized[i + 1];
|
||||||
|
if (current.end >= next.start) {
|
||||||
|
current.end = Math.max(current.start + MIN_GAP_MS, next.start - MIN_GAP_MS);
|
||||||
|
}
|
||||||
|
if (current.end <= current.start) {
|
||||||
|
current.end = current.start + MIN_GAP_MS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
/** @returns {Array<{ text: string, start: number, end: number }>} start/end 毫秒 */
|
/** @returns {Array<{ text: string, start: number, end: number }>} start/end 毫秒 */
|
||||||
function parseTranscriptionSentences(trans) {
|
function parseTranscriptionSentences(trans) {
|
||||||
const records = [];
|
const records = [];
|
||||||
@@ -273,14 +309,16 @@ function parseTranscriptionSentences(trans) {
|
|||||||
for (const s of t.sentences) {
|
for (const s of t.sentences) {
|
||||||
const text = String(s.text || "").trim();
|
const text = String(s.text || "").trim();
|
||||||
if (!text) continue;
|
if (!text) continue;
|
||||||
const start = Number(s.begin_time ?? s.start_time ?? 0);
|
const start = normalizeAsrTimeToMs(s.begin_time ?? s.start_time ?? 0);
|
||||||
const end = Number(s.end_time ?? s.end_time ?? start + 2000);
|
const end = normalizeAsrTimeToMs(
|
||||||
|
s.end_time ?? s.end ?? start + 2000,
|
||||||
|
);
|
||||||
records.push({ text, start, end });
|
records.push({ text, start, end });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return records;
|
return normalizeAsrRecords(records);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchJson(url, options = {}) {
|
async function fetchJson(url, options = {}) {
|
||||||
@@ -624,15 +662,21 @@ async function localAsr(audioPath, info) {
|
|||||||
data.records ||
|
data.records ||
|
||||||
[];
|
[];
|
||||||
const sentences = Array.isArray(records)
|
const sentences = Array.isArray(records)
|
||||||
? records
|
? normalizeAsrRecords(
|
||||||
.map((r) => {
|
records
|
||||||
const text = String(r.text || "").trim();
|
.map((r) => {
|
||||||
if (!text) return null;
|
const text = String(r.text || "").trim();
|
||||||
const start = Number(r.start ?? r.begin_time ?? 0);
|
if (!text) return null;
|
||||||
const end = Number(r.end ?? r.end_time ?? start + 2000);
|
const start = normalizeAsrTimeToMs(
|
||||||
return { text, start, end };
|
r.start ?? r.begin_time ?? 0,
|
||||||
})
|
);
|
||||||
.filter(Boolean)
|
const end = normalizeAsrTimeToMs(
|
||||||
|
r.end ?? r.end_time ?? start + 2000,
|
||||||
|
);
|
||||||
|
return { text, start, end };
|
||||||
|
})
|
||||||
|
.filter(Boolean),
|
||||||
|
)
|
||||||
: [];
|
: [];
|
||||||
const text = sentences.length
|
const text = sentences.length
|
||||||
? sentences.map((r) => r.text).join("")
|
? sentences.map((r) => r.text).join("")
|
||||||
|
|||||||
@@ -116,23 +116,117 @@ function splitScriptLines(text) {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
function alignScriptToRecords(script, records) {
|
function normalizeAsrTimeToMs(value) {
|
||||||
const lines = splitScriptLines(script);
|
const n = Number(value);
|
||||||
if (!lines.length || !records.length) return records;
|
if (!Number.isFinite(n) || n < 0) return 0;
|
||||||
|
if (n > 100) return Math.round(n);
|
||||||
|
return Math.round(n * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
if (lines.length === records.length) {
|
function normalizeAsrRecords(records) {
|
||||||
return records.map((r, i) => ({ ...r, text: lines[i] }));
|
if (!Array.isArray(records)) return [];
|
||||||
|
const normalized = records
|
||||||
|
.map((record) => {
|
||||||
|
const text = String(record?.text || record?.sentence || "").trim();
|
||||||
|
if (!text) return null;
|
||||||
|
let start = normalizeAsrTimeToMs(
|
||||||
|
record.start ?? record.begin ?? record.beginTime ?? 0,
|
||||||
|
);
|
||||||
|
let end = normalizeAsrTimeToMs(
|
||||||
|
record.end ?? record.endTime ?? start + 2000,
|
||||||
|
);
|
||||||
|
if (end <= start) end = start + 2000;
|
||||||
|
return { text, start, end };
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
const MIN_GAP_MS = 50;
|
||||||
|
for (let i = 0; i < normalized.length - 1; i++) {
|
||||||
|
const current = normalized[i];
|
||||||
|
const next = normalized[i + 1];
|
||||||
|
if (current.end >= next.start) {
|
||||||
|
current.end = Math.max(current.start + MIN_GAP_MS, next.start - MIN_GAP_MS);
|
||||||
|
}
|
||||||
|
if (current.end <= current.start) {
|
||||||
|
current.end = current.start + MIN_GAP_MS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 对齐 Electron smartLineBreak:最多 2 行,优先在标点处换行 */
|
||||||
|
function smartLineBreak(text, maxCharsPerLine = 18) {
|
||||||
|
const lines = [];
|
||||||
|
const punctuations = [",", "。", "!", "?", ";", ":", ",", ".", "!", "?", ";", ":"];
|
||||||
|
const MAX_LINES = 2;
|
||||||
|
let currentLine = "";
|
||||||
|
let pureTextLength = 0;
|
||||||
|
let i = 0;
|
||||||
|
const max = Math.max(8, Math.min(30, Number(maxCharsPerLine) || 18));
|
||||||
|
|
||||||
|
while (i < text.length) {
|
||||||
|
if (lines.length >= MAX_LINES) {
|
||||||
|
currentLine += text[i];
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
currentLine += text[i];
|
||||||
|
pureTextLength += 1;
|
||||||
|
const isPunctuation = punctuations.includes(text[i]);
|
||||||
|
const isNearMaxLength = pureTextLength >= max * 0.8;
|
||||||
|
if (isPunctuation && isNearMaxLength && lines.length < MAX_LINES - 1) {
|
||||||
|
lines.push(currentLine.trim());
|
||||||
|
currentLine = "";
|
||||||
|
pureTextLength = 0;
|
||||||
|
} else if (pureTextLength >= max && lines.length < MAX_LINES - 1) {
|
||||||
|
lines.push(currentLine.trim());
|
||||||
|
currentLine = "";
|
||||||
|
pureTextLength = 0;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if (currentLine.trim()) lines.push(currentLine.trim());
|
||||||
|
return lines.slice(0, MAX_LINES);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanSubtitleDisplayText(text) {
|
||||||
|
return String(text || "")
|
||||||
|
.replace(/[,。!?;:、""''()《》【】…—·,.!?;:'"()[\]{}<>]/g, "")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function alignScriptToRecords(script, records, videoDurationSec = 0) {
|
||||||
|
const lines = splitScriptLines(script);
|
||||||
|
const normalized = normalizeAsrRecords(records);
|
||||||
|
if (!lines.length) return normalized;
|
||||||
|
if (!normalized.length) {
|
||||||
|
return recordsFromTextEvenly(script, videoDurationSec || 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
const startMs = records[0].start;
|
if (lines.length === normalized.length) {
|
||||||
const endMs = records[records.length - 1].end;
|
return normalized.map((r, i) => ({ ...r, text: lines[i] }));
|
||||||
const total = Math.max(endMs - startMs, lines.length * 2000);
|
}
|
||||||
const step = total / lines.length;
|
|
||||||
return lines.map((text, i) => ({
|
const startMs = normalized[0].start;
|
||||||
text,
|
let endMs = normalized[normalized.length - 1].end;
|
||||||
start: Math.round(startMs + i * step),
|
const durationMs = Math.max(0, Math.round(Number(videoDurationSec) * 1000));
|
||||||
end: Math.round(startMs + (i + 1) * step),
|
if (durationMs > 0 && (endMs - startMs < 1000 || endMs < durationMs * 0.2)) {
|
||||||
}));
|
endMs = durationMs;
|
||||||
|
}
|
||||||
|
const totalMs = Math.max(endMs - startMs, lines.length * 2000);
|
||||||
|
const totalChars = lines.reduce((sum, line) => sum + line.length, 0) || lines.length;
|
||||||
|
let cursor = startMs;
|
||||||
|
|
||||||
|
return lines.map((text, index) => {
|
||||||
|
const isLast = index === lines.length - 1;
|
||||||
|
const weight = text.length / totalChars;
|
||||||
|
const dur = isLast
|
||||||
|
? Math.max(1500, endMs - cursor)
|
||||||
|
: Math.max(1500, Math.round(totalMs * weight));
|
||||||
|
const start = cursor;
|
||||||
|
const end = isLast ? endMs : Math.min(cursor + dur, endMs);
|
||||||
|
cursor = end;
|
||||||
|
return { text, start, end: Math.max(end, start + 500) };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function recordsFromTextEvenly(text, durationSec) {
|
function recordsFromTextEvenly(text, durationSec) {
|
||||||
@@ -155,14 +249,16 @@ function formatSrtTime(ms) {
|
|||||||
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")},${String(msPart).padStart(3, "0")}`;
|
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")},${String(msPart).padStart(3, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSrtContent(records, maxCharsPerLine = 20) {
|
function buildSrtContent(records, maxCharsPerLine = 18) {
|
||||||
return records
|
return records
|
||||||
.map((r, i) => {
|
.map((r, i) => {
|
||||||
const start = formatSrtTime(r.start);
|
const start = formatSrtTime(r.start);
|
||||||
const end = formatSrtTime(Math.max(r.end, r.start + 200));
|
const end = formatSrtTime(Math.max(r.end, r.start + 500));
|
||||||
const text = wrapSubtitleText(r.text, maxCharsPerLine);
|
const text = wrapSubtitleText(r.text, maxCharsPerLine);
|
||||||
|
if (!text) return "";
|
||||||
return `${i + 1}\n${start} --> ${end}\n${text}\n`;
|
return `${i + 1}\n${start} --> ${end}\n${text}\n`;
|
||||||
})
|
})
|
||||||
|
.filter(Boolean)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,16 +266,24 @@ function escapeSubPath(filePath) {
|
|||||||
return filePath.replace(/\\/g, "/").replace(/:/g, "\\:").replace(/'/g, "\\'");
|
return filePath.replace(/\\/g, "/").replace(/:/g, "\\:").replace(/'/g, "\\'");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 对齐 Electron maxCharsPerLine=20,避免单行过长铺满画面 */
|
/** 对齐 Electron maxCharsPerLine + smartLineBreak;SRT 用换行,ASS 经 assEscapeText 转 \\N */
|
||||||
function wrapSubtitleText(text, maxCharsPerLine = 20) {
|
function wrapSubtitleText(text, maxCharsPerLine = 18) {
|
||||||
const raw = String(text || "").trim();
|
const cleaned = cleanSubtitleDisplayText(text);
|
||||||
if (!raw) return "";
|
if (!cleaned) return "";
|
||||||
const max = Math.max(8, Math.min(30, Number(maxCharsPerLine) || 20));
|
return smartLineBreak(cleaned, maxCharsPerLine).join("\n");
|
||||||
const lines = [];
|
}
|
||||||
for (let i = 0; i < raw.length; i += max) {
|
|
||||||
lines.push(raw.slice(i, i + max));
|
function logSubtitle(level, msg, fields) {
|
||||||
|
const tag = `[autoGenerateSubtitleAndBGM] ${msg}`;
|
||||||
|
if (globalThis.__native?.log) {
|
||||||
|
globalThis.__native.log(level, tag, fields ?? null);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return lines.join("\n");
|
if (fields != null) {
|
||||||
|
if (level === "error") console.error(tag, fields);
|
||||||
|
else console.log(tag, fields);
|
||||||
|
} else if (level === "error") console.error(tag);
|
||||||
|
else console.log(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hexToAssColor(hex) {
|
function hexToAssColor(hex) {
|
||||||
@@ -216,21 +320,21 @@ function normalizeBurnStyle(style) {
|
|||||||
backgroundOpacity: Number(s.backgroundOpacity) ?? 0,
|
backgroundOpacity: Number(s.backgroundOpacity) ?? 0,
|
||||||
position: String(s.position || "bottom"),
|
position: String(s.position || "bottom"),
|
||||||
fontSizeScale: Number(s.fontSizeScale) || 100,
|
fontSizeScale: Number(s.fontSizeScale) || 100,
|
||||||
maxCharsPerLine: Number(s.maxCharsPerLine) || 20,
|
maxCharsPerLine: Number(s.maxCharsPerLine) || 18,
|
||||||
hasBg,
|
hasBg,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveAssLayout(position, videoHeight) {
|
function resolveAssLayout(position, videoHeight) {
|
||||||
const h = Math.max(360, Number(videoHeight) || 1080);
|
const h = Math.max(360, Number(videoHeight) || 1080);
|
||||||
const marginV = Math.round(48 * (h / 1080));
|
const marginV = 80;
|
||||||
if (position === "top") {
|
if (position === "top") {
|
||||||
return { alignment: 8, marginV };
|
return { alignment: 8, marginL: 120, marginR: 120, marginV };
|
||||||
}
|
}
|
||||||
if (position === "center") {
|
if (position === "center") {
|
||||||
return { alignment: 5, marginV: 0 };
|
return { alignment: 5, marginL: 120, marginR: 120, marginV: 0 };
|
||||||
}
|
}
|
||||||
return { alignment: 2, marginV };
|
return { alignment: 2, marginL: 120, marginR: 120, marginV };
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAssContent(records, styleInput, videoWidth, videoHeight) {
|
function buildAssContent(records, styleInput, videoWidth, videoHeight) {
|
||||||
@@ -240,9 +344,13 @@ function buildAssContent(records, styleInput, videoWidth, videoHeight) {
|
|||||||
const scale = Math.max(0.2, Math.min(2, style.fontSizeScale / 100));
|
const scale = Math.max(0.2, Math.min(2, style.fontSizeScale / 100));
|
||||||
const fontSize = Math.max(14, Math.round(style.fontSize * scale));
|
const fontSize = Math.max(14, Math.round(style.fontSize * scale));
|
||||||
const outline = Math.max(0, Math.round(style.outlineWidth));
|
const outline = Math.max(0, Math.round(style.outlineWidth));
|
||||||
const { alignment, marginV } = resolveAssLayout(style.position, height);
|
const { alignment, marginL, marginR, marginV } = resolveAssLayout(
|
||||||
|
style.position,
|
||||||
|
height,
|
||||||
|
);
|
||||||
const hasBg = style.hasBg;
|
const hasBg = style.hasBg;
|
||||||
const borderStyle = hasBg ? 4 : 1;
|
const borderStyle = hasBg ? 3 : 1;
|
||||||
|
const finalOutline = hasBg ? 1 : outline;
|
||||||
const backColour = hasBg
|
const backColour = hasBg
|
||||||
? hexToAssColor(style.backgroundColor)
|
? hexToAssColor(style.backgroundColor)
|
||||||
: "&H00000000";
|
: "&H00000000";
|
||||||
@@ -260,7 +368,7 @@ function buildAssContent(records, styleInput, videoWidth, videoHeight) {
|
|||||||
"",
|
"",
|
||||||
"[V4+ Styles]",
|
"[V4+ Styles]",
|
||||||
"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding",
|
"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding",
|
||||||
`Style: Default,${style.fontName},${fontSize},${primary},${primary},${outlineColour},${backColour},0,0,0,0,100,100,0,0,${borderStyle},${outline},0,${alignment},20,20,${marginV},1`,
|
`Style: Default,${style.fontName},${fontSize},${primary},${primary},${outlineColour},${backColour},0,0,0,0,100,100,0,0,${borderStyle},${finalOutline},0,${alignment},${marginL},${marginR},${marginV},1`,
|
||||||
"",
|
"",
|
||||||
"[Events]",
|
"[Events]",
|
||||||
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
|
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
|
||||||
@@ -268,10 +376,11 @@ function buildAssContent(records, styleInput, videoWidth, videoHeight) {
|
|||||||
|
|
||||||
const events = records.map((r) => {
|
const events = records.map((r) => {
|
||||||
const start = formatAssTime(r.start);
|
const start = formatAssTime(r.start);
|
||||||
const end = formatAssTime(Math.max(r.end, r.start + 200));
|
const end = formatAssTime(Math.max(r.end, r.start + 500));
|
||||||
const text = assEscapeText(wrapSubtitleText(r.text, style.maxCharsPerLine));
|
const text = assEscapeText(wrapSubtitleText(r.text, style.maxCharsPerLine));
|
||||||
|
if (!text) return "";
|
||||||
return `Dialogue: 0,${start},${end},Default,,0,0,0,,${text}`;
|
return `Dialogue: 0,${start},${end},Default,,0,0,0,,${text}`;
|
||||||
});
|
}).filter(Boolean);
|
||||||
|
|
||||||
return `${header.join("\n")}\n${events.join("\n")}\n`;
|
return `${header.join("\n")}\n${events.join("\n")}\n`;
|
||||||
}
|
}
|
||||||
@@ -398,16 +507,27 @@ globalThis.__nodejsMain = async function main(params) {
|
|||||||
globalThis.__native?.emitProgress?.("正在提取音频…");
|
globalThis.__native?.emitProgress?.("正在提取音频…");
|
||||||
const audioPath = await pipelineNative.ffmpegExtractAudio(current);
|
const audioPath = await pipelineNative.ffmpegExtractAudio(current);
|
||||||
const scriptContent = String(p.scriptContent || "").trim();
|
const scriptContent = String(p.scriptContent || "").trim();
|
||||||
|
const videoDuration = probeVideoDuration(current);
|
||||||
|
logSubtitle("info", "视频时长(秒)", { videoDuration });
|
||||||
const { records: rawRecords } = await runAsr(audioPath, modelConfig, scriptContent);
|
const { records: rawRecords } = await runAsr(audioPath, modelConfig, scriptContent);
|
||||||
|
logSubtitle("info", "ASR 原始句数", { count: rawRecords?.length ?? 0 });
|
||||||
|
|
||||||
let records = rawRecords;
|
let records = normalizeAsrRecords(rawRecords);
|
||||||
|
logSubtitle("info", "时间轴归一化后", {
|
||||||
|
count: records.length,
|
||||||
|
spanMs:
|
||||||
|
records.length > 1
|
||||||
|
? records[records.length - 1].end - records[0].start
|
||||||
|
: 0,
|
||||||
|
});
|
||||||
if (p.smartSubtitle !== false && scriptContent) {
|
if (p.smartSubtitle !== false && scriptContent) {
|
||||||
records = alignScriptToRecords(scriptContent, records);
|
records = alignScriptToRecords(scriptContent, records, videoDuration);
|
||||||
|
logSubtitle("info", "智能字幕对齐后", { count: records.length });
|
||||||
}
|
}
|
||||||
if (!records.length) {
|
if (!records.length) {
|
||||||
const duration = probeVideoDuration(current);
|
|
||||||
const fallbackText = scriptContent || " ";
|
const fallbackText = scriptContent || " ";
|
||||||
records = recordsFromTextEvenly(fallbackText, duration || 30);
|
records = recordsFromTextEvenly(fallbackText, videoDuration || 30);
|
||||||
|
logSubtitle("info", "按文案均分时间轴", { count: records.length });
|
||||||
}
|
}
|
||||||
if (!records.length) {
|
if (!records.length) {
|
||||||
return { success: false, error: "未识别到语音内容,无法生成字幕" };
|
return { success: false, error: "未识别到语音内容,无法生成字幕" };
|
||||||
@@ -417,7 +537,7 @@ globalThis.__nodejsMain = async function main(params) {
|
|||||||
p.subtitleStyle && typeof p.subtitleStyle === "object"
|
p.subtitleStyle && typeof p.subtitleStyle === "object"
|
||||||
? p.subtitleStyle
|
? p.subtitleStyle
|
||||||
: null;
|
: null;
|
||||||
const maxChars = burnStyle?.maxCharsPerLine || 20;
|
const maxChars = burnStyle?.maxCharsPerLine || 18;
|
||||||
|
|
||||||
srtPath = makeOutputPath("subtitle", "srt");
|
srtPath = makeOutputPath("subtitle", "srt");
|
||||||
fs.writeFileSync(srtPath, buildSrtContent(records, maxChars), "utf8");
|
fs.writeFileSync(srtPath, buildSrtContent(records, maxChars), "utf8");
|
||||||
|
|||||||
Reference in New Issue
Block a user