1
This commit is contained in:
176
scripts/nodejs/mixcut_replacements.js
Normal file
176
scripts/nodejs/mixcut_replacements.js
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const IMAGE_EXT = new Set([".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif"]);
|
||||||
|
|
||||||
|
function isImageFile(filePath) {
|
||||||
|
return IMAGE_EXT.has(path.extname(filePath).toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function listMediaFiles(folderPath) {
|
||||||
|
if (!folderPath || !fs.existsSync(folderPath)) return [];
|
||||||
|
const entries = fs.readdirSync(folderPath, { withFileTypes: true });
|
||||||
|
const files = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isFile()) continue;
|
||||||
|
const ext = path.extname(entry.name).toLowerCase();
|
||||||
|
if (
|
||||||
|
[".mp4", ".mov", ".webm", ".mkv", ".m4v", ".avi", ...IMAGE_EXT].includes(ext)
|
||||||
|
) {
|
||||||
|
files.push(path.join(folderPath, entry.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickFromFolder(selectionMode, folderPath, specifiedPath) {
|
||||||
|
if (selectionMode === "specified" && specifiedPath) {
|
||||||
|
return specifiedPath;
|
||||||
|
}
|
||||||
|
const files = listMediaFiles(folderPath);
|
||||||
|
if (!files.length) return null;
|
||||||
|
if (selectionMode === "sequential") {
|
||||||
|
return files[0];
|
||||||
|
}
|
||||||
|
return files[Math.floor(Math.random() * files.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSubtitleModeReplacements(records, subtitleModeConfig, probeVideoDuration) {
|
||||||
|
const mappings = Array.isArray(subtitleModeConfig?.mappings)
|
||||||
|
? subtitleModeConfig.mappings
|
||||||
|
: [];
|
||||||
|
const replacements = [];
|
||||||
|
|
||||||
|
for (const mapping of mappings) {
|
||||||
|
if (!mapping?.materialPath || !fs.existsSync(mapping.materialPath)) continue;
|
||||||
|
const indices = Array.isArray(mapping.subtitleIndices)
|
||||||
|
? mapping.subtitleIndices
|
||||||
|
: [];
|
||||||
|
if (!indices.length || !records.length) continue;
|
||||||
|
|
||||||
|
let minStart = Infinity;
|
||||||
|
let maxEnd = -Infinity;
|
||||||
|
for (const idx of indices) {
|
||||||
|
const record = records[idx];
|
||||||
|
if (!record) continue;
|
||||||
|
const startSec = Number(record.start ?? record.startTime ?? 0) / 1000;
|
||||||
|
const endSec = Number(record.end ?? record.endTime ?? 0) / 1000;
|
||||||
|
minStart = Math.min(minStart, startSec);
|
||||||
|
maxEnd = Math.max(maxEnd, endSec);
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(minStart) || !Number.isFinite(maxEnd)) continue;
|
||||||
|
const duration = maxEnd - minStart;
|
||||||
|
if (duration <= 0) continue;
|
||||||
|
|
||||||
|
const isImage = mapping.isImage ?? isImageFile(mapping.materialPath);
|
||||||
|
const materialDuration = isImage
|
||||||
|
? duration
|
||||||
|
: probeVideoDuration(mapping.materialPath);
|
||||||
|
|
||||||
|
replacements.push({
|
||||||
|
startTime: minStart,
|
||||||
|
duration,
|
||||||
|
materialPath: mapping.materialPath,
|
||||||
|
materialDuration,
|
||||||
|
materialStartOffset: 0,
|
||||||
|
displayMode: mapping.displayMode || "fullscreen",
|
||||||
|
pipPosition: mapping.pipPosition || "bottom-right",
|
||||||
|
pipSizePercent: mapping.pipSizePercent ?? 30,
|
||||||
|
pipScaleMode: mapping.pipScaleMode || "fit",
|
||||||
|
isImage,
|
||||||
|
originalVideoMinimized: mapping.originalVideoMinimized,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return replacements.sort((a, b) => a.startTime - b.startTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFixedModeReplacements(fixedModeConfig, videoDuration, probeVideoDuration) {
|
||||||
|
const replacements = [];
|
||||||
|
if (!fixedModeConfig?.enabled) return replacements;
|
||||||
|
|
||||||
|
const segments = [
|
||||||
|
{ key: "opening", reason: "片头" },
|
||||||
|
{ key: "middle", reason: "片中" },
|
||||||
|
{ key: "ending", reason: "片尾" },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { key, reason } of segments) {
|
||||||
|
const seg = fixedModeConfig[key];
|
||||||
|
if (!seg?.enabled) continue;
|
||||||
|
|
||||||
|
const min = Number(seg.timeRange?.durationMin ?? 2);
|
||||||
|
const max = Number(seg.timeRange?.durationMax ?? 5);
|
||||||
|
let startTime = 0;
|
||||||
|
let duration = max - min;
|
||||||
|
if (key === "middle") {
|
||||||
|
const mid = videoDuration / 2;
|
||||||
|
startTime = Math.max(0, mid + min);
|
||||||
|
duration = Math.min(videoDuration, mid + max) - startTime;
|
||||||
|
} else if (key === "ending") {
|
||||||
|
duration = max - min;
|
||||||
|
startTime = Math.max(0, videoDuration - duration);
|
||||||
|
} else {
|
||||||
|
startTime = min;
|
||||||
|
duration = max - min;
|
||||||
|
}
|
||||||
|
if (duration <= 0 || startTime >= videoDuration) continue;
|
||||||
|
|
||||||
|
const materialPath = pickFromFolder(
|
||||||
|
seg.selectionMode || "random",
|
||||||
|
seg.materialFolderPath,
|
||||||
|
seg.specifiedMaterialPath,
|
||||||
|
);
|
||||||
|
if (!materialPath || !fs.existsSync(materialPath)) continue;
|
||||||
|
|
||||||
|
const isImage = isImageFile(materialPath);
|
||||||
|
replacements.push({
|
||||||
|
startTime,
|
||||||
|
duration,
|
||||||
|
materialPath,
|
||||||
|
materialDuration: isImage ? duration : probeVideoDuration(materialPath),
|
||||||
|
materialStartOffset: 0,
|
||||||
|
displayMode: seg.displayMode || "fullscreen",
|
||||||
|
pipPosition: seg.pipPosition || "bottom-right",
|
||||||
|
pipSizePercent: seg.pipSizePercent ?? 30,
|
||||||
|
pipScaleMode: seg.pipScaleMode || "fit",
|
||||||
|
isImage,
|
||||||
|
originalVideoMinimized: seg.originalVideoMinimized,
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return replacements.sort((a, b) => a.startTime - b.startTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReplacementsFromMixCutSettings(settings, videoDuration, probeVideoDuration) {
|
||||||
|
if (!settings || typeof settings !== "object" || !settings.enabled) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const mode = settings.mode || "subtitle";
|
||||||
|
let replacements = [];
|
||||||
|
|
||||||
|
if (mode === "subtitle") {
|
||||||
|
const records = Array.isArray(settings.records) ? settings.records : [];
|
||||||
|
replacements = buildSubtitleModeReplacements(
|
||||||
|
records,
|
||||||
|
settings.subtitleModeConfig,
|
||||||
|
probeVideoDuration,
|
||||||
|
);
|
||||||
|
} else if (mode === "fixed" && settings.fixedModeConfig?.enabled) {
|
||||||
|
replacements = buildFixedModeReplacements(
|
||||||
|
settings.fixedModeConfig,
|
||||||
|
videoDuration,
|
||||||
|
probeVideoDuration,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return replacements;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
buildReplacementsFromMixCutSettings,
|
||||||
|
buildSubtitleModeReplacements,
|
||||||
|
buildFixedModeReplacements,
|
||||||
|
};
|
||||||
@@ -422,6 +422,16 @@ globalThis.__nodejsMain = async function main(params) {
|
|||||||
srtPath = makeOutputPath("subtitle", "srt");
|
srtPath = makeOutputPath("subtitle", "srt");
|
||||||
fs.writeFileSync(srtPath, buildSrtContent(records, maxChars), "utf8");
|
fs.writeFileSync(srtPath, buildSrtContent(records, maxChars), "utf8");
|
||||||
|
|
||||||
|
if (p.recognizeOnly) {
|
||||||
|
pipelineNative.deleteFile(audioPath);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
records,
|
||||||
|
srtPath,
|
||||||
|
message: `字幕识别完成,共 ${records.length} 条`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const { width, height } = probeVideoSize(current);
|
const { width, height } = probeVideoSize(current);
|
||||||
const assPath = makeOutputPath("subtitle", "ass");
|
const assPath = makeOutputPath("subtitle", "ass");
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ const fs = require("fs");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
const { spawnSync } = require("child_process");
|
const { spawnSync } = require("child_process");
|
||||||
|
const {
|
||||||
|
buildReplacementsFromMixCutSettings,
|
||||||
|
} = require("./mixcut_replacements.js");
|
||||||
|
|
||||||
const IMAGE_EXT = new Set([".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif"]);
|
const IMAGE_EXT = new Set([".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif"]);
|
||||||
|
|
||||||
@@ -434,6 +437,14 @@ function normalizeReplacements(raw) {
|
|||||||
|
|
||||||
function pickReplacementsFromSettings(settings, videoDuration) {
|
function pickReplacementsFromSettings(settings, videoDuration) {
|
||||||
if (!settings || typeof settings !== "object") return [];
|
if (!settings || typeof settings !== "object") return [];
|
||||||
|
const fromMixCut = buildReplacementsFromMixCutSettings(
|
||||||
|
settings,
|
||||||
|
videoDuration,
|
||||||
|
probeVideoDuration,
|
||||||
|
);
|
||||||
|
if (fromMixCut.length) {
|
||||||
|
return normalizeReplacements(fromMixCut);
|
||||||
|
}
|
||||||
if (Array.isArray(settings.replacements) && settings.replacements.length) {
|
if (Array.isArray(settings.replacements) && settings.replacements.length) {
|
||||||
return normalizeReplacements(settings.replacements);
|
return normalizeReplacements(settings.replacements);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user