11
This commit is contained in:
213
src/utils/mixCutDefaults.js
Normal file
213
src/utils/mixCutDefaults.js
Normal 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
72
src/utils/srtParser.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 解析 SRT 字幕为混剪 records(start/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)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user