This commit is contained in:
949036910@qq.com
2026-06-02 21:52:12 +08:00
parent d637088a34
commit a2c01f306f
6 changed files with 291 additions and 31 deletions

View File

@@ -37,19 +37,63 @@ function escapeDrawtext(text) {
.replace(/%/g, "\\%");
}
/** ffmpeg drawtext 路径转义Windows 盘符冒号) */
function escapeFfmpegPath(filePath) {
return String(filePath).replace(/\\/g, "/").replace(/:/g, "\\:");
}
/**
* 定位系统中文字体ffmpeg drawtext 必须指定 fontfile
*/
function locateCjkFontFile() {
const scriptsDir = process.env.AICLIENT_COVER_SCRIPTS_DIR || "";
const windir = process.env.WINDIR || "C:\\Windows";
const candidates = [
path.join(scriptsDir, "fonts", "simhei.ttf"),
path.join(scriptsDir, "fonts", "msyh.ttc"),
path.join(windir, "Fonts", "simhei.ttf"),
path.join(windir, "Fonts", "msyh.ttc"),
path.join(windir, "Fonts", "simsun.ttc"),
"C:/Windows/Fonts/simhei.ttf",
"C:/Windows/Fonts/msyh.ttc",
];
for (const p of candidates) {
if (p && fs.existsSync(p)) return p;
}
return null;
}
function buildTextPosition(titlePosition) {
if (titlePosition === "top") return "y=50";
if (titlePosition === "center") return "y=(h-text_h)/2";
return "y=h-text_h-50";
}
function runFfmpegCover({ sourceVideo, titleText, effectStyle, coverPath, tempFramePath, emit }) {
const IMAGE_EXT = new Set([".jpg", ".jpeg", ".png", ".bmp", ".webp"]);
function isImagePath(filePath) {
return IMAGE_EXT.has(path.extname(String(filePath || "")).toLowerCase());
}
function prepareFrameFromSource(sourcePath, tempFramePath, emit) {
if (isImagePath(sourcePath)) {
emit?.("正在加载图片素材…");
execFfmpeg([
"-i",
sourcePath,
"-vf",
"scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2:color=black",
"-y",
tempFramePath,
]);
return;
}
emit?.("正在提取视频帧…");
execFfmpeg([
"-ss",
"00:00:03",
"-i",
sourceVideo,
sourcePath,
"-vframes",
"1",
"-pix_fmt",
@@ -63,16 +107,41 @@ function runFfmpegCover({ sourceVideo, titleText, effectStyle, coverPath, tempFr
"-y",
tempFramePath,
]);
}
function runFfmpegCover({ sourceVideo, titleText, effectStyle, coverPath, tempFramePath, emit }) {
prepareFrameFromSource(sourceVideo, tempFramePath, emit);
const titleFontSize = effectStyle.titleFontSize ?? 90;
const titleFontColor = effectStyle.titleFontColor || "#FFFFFF";
const titlePosition = effectStyle.titlePosition || "bottom";
const textPos = buildTextPosition(titlePosition);
const escaped = escapeDrawtext(titleText);
const vf = `drawtext=text='${escaped}':fontsize=${titleFontSize}:fontcolor=${titleFontColor}:x=(w-text_w)/2:${textPos}:shadowcolor=black:shadowx=2:shadowy=2`;
const fontFile = locateCjkFontFile();
if (!fontFile) {
throw new Error("未找到中文字体文件,无法使用 ffmpeg 绘制标题(请确认 C:\\Windows\\Fonts\\simhei.ttf 存在)");
}
const titleFilePath = path.join(
OUTPUT_DIR,
`cover_title_${Date.now()}.txt`,
);
fs.writeFileSync(titleFilePath, String(titleText), { encoding: "utf8" });
const fontOpt = `fontfile='${escapeFfmpegPath(fontFile)}'`;
const textOpt = `textfile='${escapeFfmpegPath(titleFilePath)}'`;
const vf = `drawtext=${fontOpt}:${textOpt}:fontsize=${titleFontSize}:fontcolor=${titleFontColor}:x=(w-text_w)/2:${textPos}:shadowcolor=black:shadowx=2:shadowy=2`;
emit?.("正在叠加标题文字ffmpeg…");
execFfmpeg(["-i", tempFramePath, "-vf", vf, "-y", coverPath]);
try {
execFfmpeg(["-i", tempFramePath, "-vf", vf, "-y", coverPath]);
} finally {
try {
fs.unlinkSync(titleFilePath);
} catch {
/* ignore */
}
}
}
function shouldUsePilCover(effectStyle) {