This commit is contained in:
949036910@qq.com
2026-05-31 02:29:24 +08:00
parent e042f15646
commit 736f1f9014
14 changed files with 348699 additions and 70 deletions

213
src/utils/mixCutDefaults.js Normal file
View File

@@ -0,0 +1,213 @@
/** @typedef {'subtitle' | 'fixed' | 'keyword'} MixCutMode */
/**
* @returns {{ enabled: boolean, shape: string, position: string, sizePercent: number }}
*/
export function createDefaultOriginalVideoMinimized() {
return {
enabled: false,
shape: "square",
position: "bottom-right",
sizePercent: 25,
};
}
/**
* @returns {object}
*/
export function createDefaultSegmentConfig() {
return {
enabled: false,
timeRange: { durationMin: 2, durationMax: 5 },
materialFolderPath: "",
specifiedMaterialPath: "",
selectionMode: "random",
displayMode: "fullscreen",
pipPosition: "bottom-right",
pipSizePercent: 30,
pipScaleMode: "fit",
originalVideoMinimized: createDefaultOriginalVideoMinimized(),
};
}
/**
* @returns {object}
*/
export function createDefaultFixedModeConfig() {
return {
enabled: false,
opening: createDefaultSegmentConfig(),
middle: {
...createDefaultSegmentConfig(),
pipPosition: "bottom-left",
pipSizePercent: 40,
},
ending: createDefaultSegmentConfig(),
};
}
/**
* @returns {object}
*/
export function createDefaultKeywordGroup(groupId, groupName) {
return {
groupId,
groupName,
enabled: false,
materialFolderPath: "",
specifiedMaterialPath: "",
selectionMode: "random",
durationMin: 2,
durationMax: 5,
displayMode: "fullscreen",
pipPosition: "bottom-right",
pipSizePercent: 30,
pipScaleMode: "fit",
originalVideoMinimized: createDefaultOriginalVideoMinimized(),
};
}
/**
* @returns {object}
*/
export function createDefaultKeywordModeConfig() {
return {
enabled: false,
groups: [],
};
}
/**
* @returns {object}
*/
export function createDefaultSubtitleModeConfig() {
return {
enabled: false,
mappings: [],
};
}
/**
* @returns {object}
*/
export function createDefaultGlobalDisplay() {
return {
displayMode: "fullscreen",
pipPosition: "bottom-right",
pipSizePercent: 30,
pipScaleMode: "fit",
originalVideoMinimized: createDefaultOriginalVideoMinimized(),
};
}
/**
* @returns {object}
*/
export function createDefaultMixCutSettings() {
return {
enabled: false,
mode: "subtitle",
displayMode: "pip",
records: [],
subtitleFilePath: "",
fixedModeConfig: createDefaultFixedModeConfig(),
keywordModeConfig: createDefaultKeywordModeConfig(),
subtitleModeConfig: createDefaultSubtitleModeConfig(),
};
}
/**
* @param {object | null | undefined} raw
*/
export function normalizeMixCutSettings(raw) {
const base = createDefaultMixCutSettings();
if (!raw || typeof raw !== "object") return base;
const mergeSegment = (target, source) => {
if (!source || typeof source !== "object") return target;
return {
...target,
...source,
timeRange: { ...target.timeRange, ...(source.timeRange || {}) },
originalVideoMinimized: {
...target.originalVideoMinimized,
...(source.originalVideoMinimized || {}),
},
};
};
const fixed = createDefaultFixedModeConfig();
const fixedRaw = raw.fixedModeConfig || {};
fixed.enabled = Boolean(fixedRaw.enabled);
fixed.opening = mergeSegment(fixed.opening, fixedRaw.opening);
fixed.middle = mergeSegment(fixed.middle, fixedRaw.middle);
fixed.ending = mergeSegment(fixed.ending, fixedRaw.ending);
const keyword = createDefaultKeywordModeConfig();
const keywordRaw = raw.keywordModeConfig || {};
keyword.enabled = Boolean(keywordRaw.enabled);
keyword.groups = Array.isArray(keywordRaw.groups)
? keywordRaw.groups.map((g, i) => ({
...createDefaultKeywordGroup(g.groupId || `group_${i}`, g.groupName || `分组 ${i + 1}`),
...g,
originalVideoMinimized: {
...createDefaultOriginalVideoMinimized(),
...(g.originalVideoMinimized || {}),
},
}))
: [];
const subtitle = createDefaultSubtitleModeConfig();
const subtitleRaw = raw.subtitleModeConfig || {};
subtitle.enabled = Boolean(subtitleRaw.enabled);
subtitle.mappings = Array.isArray(subtitleRaw.mappings)
? subtitleRaw.mappings.map((m) => ({
...m,
subtitleIndices: Array.isArray(m.subtitleIndices) ? [...m.subtitleIndices] : [],
originalVideoMinimized: {
...createDefaultOriginalVideoMinimized(),
...(m.originalVideoMinimized || {}),
},
}))
: [];
return {
...base,
...raw,
enabled: Boolean(raw.enabled),
mode: raw.mode === "fixed" || raw.mode === "keyword" ? raw.mode : "subtitle",
displayMode: raw.displayMode === "fullscreen" ? "fullscreen" : "pip",
records: Array.isArray(raw.records) ? raw.records : [],
subtitleFilePath: String(raw.subtitleFilePath || ""),
fixedModeConfig: fixed,
keywordModeConfig: keyword,
subtitleModeConfig: subtitle,
simplePip: raw.simplePip,
};
}
export const PIP_POSITION_OPTIONS = [
{ label: "左上角", value: "top-left" },
{ label: "顶部居中", value: "top-center" },
{ label: "右上角", value: "top-right" },
{ label: "左侧居中", value: "center-left" },
{ label: "正中间", value: "center" },
{ label: "右侧居中", value: "center-right" },
{ label: "左下角", value: "bottom-left" },
{ label: "底部居中", value: "bottom-center" },
{ label: "右下角", value: "bottom-right" },
];
export const MINIMIZED_POSITION_OPTIONS = [
{ label: "左上角", value: "top-left" },
{ label: "右上角", value: "top-right" },
{ label: "左下角", value: "bottom-left" },
{ label: "右下角", value: "bottom-right" },
{ label: "正中间", value: "center" },
];
export const SELECTION_MODE_OPTIONS = [
{ label: "随机", value: "random" },
{ label: "顺序", value: "sequential" },
{ label: "指定素材", value: "specified" },
];

72
src/utils/srtParser.js Normal file
View File

@@ -0,0 +1,72 @@
/**
* 解析 SRT 字幕为混剪 recordsstart/end 单位:毫秒)
* @param {string} content
* @returns {Array<{ text: string, start: number, end: number }>}
*/
export function parseSrtToRecords(content) {
const text = String(content || "").replace(/\uFEFF/g, "").trim();
if (!text) return [];
const blocks = text.split(/\n\s*\n+/);
const records = [];
for (const block of blocks) {
const lines = block.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
if (lines.length < 2) continue;
let timeLineIdx = 0;
if (/^\d+$/.test(lines[0])) {
timeLineIdx = 1;
}
const timeLine = lines[timeLineIdx];
if (!timeLine || !timeLine.includes("-->")) continue;
const [startRaw, endRaw] = timeLine.split("-->").map((s) => s.trim());
const start = parseSrtTimeToMs(startRaw);
const end = parseSrtTimeToMs(endRaw);
const body = lines.slice(timeLineIdx + 1).join("\n").trim();
if (!body || !Number.isFinite(start) || !Number.isFinite(end)) continue;
records.push({
text: body.replace(/<[^>]+>/g, ""),
start,
end: Math.max(end, start + 200),
});
}
return records;
}
/**
* @param {string} raw
* @returns {number}
*/
function parseSrtTimeToMs(raw) {
const m = String(raw || "").trim().match(
/(?:(\d+):)?(\d{2}):(\d{2})[.,](\d{1,3})/,
);
if (!m) return NaN;
const h = Number(m[1] || 0);
const min = Number(m[2]);
const sec = Number(m[3]);
const msPart = String(m[4]).padEnd(3, "0").slice(0, 3);
return h * 3600000 + min * 60000 + sec * 1000 + Number(msPart);
}
/**
* @param {number} ms
* @returns {string}
*/
export function formatRecordTimeRange(msStart, msEnd) {
return `${formatShortTime(msStart)} - ${formatShortTime(msEnd)}`;
}
/**
* @param {number} ms
*/
function formatShortTime(ms) {
const totalSec = Math.max(0, ms) / 1000;
const min = Math.floor(totalSec / 60);
const sec = totalSec - min * 60;
return `${min}:${sec.toFixed(1)}`;
}