Commit 18a303ab authored by liujiangnan's avatar liujiangnan

feat: 字幕生成

parent bcbe92bd
......@@ -221,18 +221,13 @@
</el-page-header>
</span>
<div
v-loading="subtitleEditorLoading || isAnyTaskRunning()"
:element-loading-text="isAnyTaskRunning() ? '任务进行中,编辑已锁定' : ''"
v-loading="subtitleEditorLoading"
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.5)"
>
<div style="margin-bottom:12px;">
<!-- <el-button type="primary" size="mini" @click="applyEditorChanges">保存修改</el-button> -->
<el-tooltip :content="isAnyTaskRunning() ? '有任务进行中,完成后可操作' : ''" :disabled="!isAnyTaskRunning()" placement="top">
<span class="disabled-tip">
<el-button type="primary" size="mini" :disabled="isAnyTaskRunning()" @click="overwriteAndRegenerateLyrics">覆盖上传并重新生成歌词</el-button>
</span>
</el-tooltip>
<el-button type="primary" size="mini" @click="overwriteAndRegenerateLyrics">覆盖上传并重新生成歌词</el-button>
</div>
<div v-if="subtitleEditor && subtitleEditor.mergedList">
<el-timeline>
......
......@@ -28,13 +28,13 @@ document.addEventListener('DOMContentLoaded', () => {
window.courseware.getData(function (data) {
window.air.hideAirClassLoading(key, data);
if (!data) {
// 加载默认数据
$.getJSON('../play/defaultData.json', function (defaultData) {
initVueApp(defaultData);
})
return;
}
// if (!data) {
// // 加载默认数据
// $.getJSON('../play/defaultData.json', function (defaultData) {
// initVueApp(defaultData);
// })
// return;
// }
//数据加载完,才算页面加载完成
initVueApp(data);
}, key);
......@@ -1061,50 +1061,52 @@ function initVueApp(savedData) {
// 应用修改回原始数组
applyEditorChanges() {
const merged = this.subtitleEditor.mergedList || [];
const sList = this.subtitleEditor.subtitleList || [];
const tList = this.subtitleEditor.translatedList || [];
const len = merged.length;
let invalidJsonCount = 0;
for (let i = 0; i < len; i++) {
// 关键修复:根据 mergedList 重新构建数组,确保“拆分后的第二句”被真实写回
const newSList = [];
const newTList = [];
// 强制使用“数值秒 Number + 三位小数”的格式
const sFmt = { kind: 'number', unit: 'sec', decimals: 3 };
const tFmt = { kind: 'number', unit: 'sec', decimals: 3 };
for (let i = 0; i < merged.length; i++) {
const m = merged[i];
if (!m) continue;
// 写回字幕数组
const sIdx = (m.sIndex != null ? m.sIndex : i);
if (!sList[sIdx]) sList[sIdx] = {};
// 保持分词引用写回
if (Array.isArray(m.sWordsRef)) {
// 确保使用 words 字段存储
sList[sIdx].words = m.sWordsRef;
// 根据分词动态刷新句子 text
sList[sIdx].text = this.buildSentenceFromWords(m.sWordsRef);
}
// 写回翻译数组
let tIdx = (m.tIndex != null ? m.tIndex : sIdx);
if (!tList[tIdx]) {
// 如果对应索引不存在,则在该索引位创建(稀疏也可)
tList[tIdx] = {};
}
if (Array.isArray(m.tItemsRef)) {
tList[tIdx].translatedItems = m.tItemsRef;
// 根据分段动态刷新整句 translated
tList[tIdx].translated = this.buildSentenceFromTranslatedItems(m.tItemsRef);
}
}
// 清空被合并项的原始内容,避免导出时出现重复
const clearedS = Array.isArray(this.subtitleEditor.clearedSIndices) ? this.subtitleEditor.clearedSIndices : [];
const clearedT = Array.isArray(this.subtitleEditor.clearedTIndices) ? this.subtitleEditor.clearedTIndices : [];
for (const si of clearedS) {
if (sList[si]) {
sList[si].words = [];
sList[si].text = '';
}
}
for (const ti of clearedT) {
if (tList[ti]) {
tList[ti].translatedItems = [];
tList[ti].translated = '';
}
// 构建字幕条目
const sWords = Array.isArray(m.sWordsRef) ? m.sWordsRef : [];
// 计算 start/end(优先用分词的边界,否则回退到 m.start/m.end),以秒为内部统一单位
let sStartSec = this.parseTimeToSeconds(sWords.length ? sWords[0].start : m.start);
let sEndSec = this.parseTimeToSeconds(sWords.length ? sWords[sWords.length - 1].end : m.end);
// 底限保护
if (isNaN(sStartSec) && !isNaN(this.parseTimeToSeconds(m.start))) sStartSec = this.parseTimeToSeconds(m.start);
if (isNaN(sEndSec) && !isNaN(this.parseTimeToSeconds(m.end))) sEndSec = this.parseTimeToSeconds(m.end);
// 写回时恢复为原始格式
const sStartOut = this.serializeTimeByFormat(sStartSec, sFmt);
const sEndOut = this.serializeTimeByFormat(sEndSec, sFmt);
newSList.push({
start: sStartOut,
end: sEndOut,
words: sWords,
text: this.buildSentenceFromWords(sWords),
});
// 构建翻译条目
const tSegs = Array.isArray(m.tItemsRef) ? m.tItemsRef : [];
// 翻译的 start/end 也尽量从分段计算(内部仍以秒为单位)
let tStartSec = this.parseTimeToSeconds(tSegs.length ? tSegs[0].start : sStartSec);
let tEndSec = this.parseTimeToSeconds(tSegs.length ? tSegs[tSegs.length - 1].end : sEndSec);
const tStartOut = this.serializeTimeByFormat(tStartSec, tFmt);
const tEndOut = this.serializeTimeByFormat(tEndSec, tFmt);
newTList.push({
start: tStartOut,
end: tEndOut,
translatedItems: tSegs,
translated: this.buildSentenceFromTranslatedItems(tSegs),
});
}
// 用新数组覆盖旧数组(确保下一步导出/覆盖时包含拆分后的完整结果)
this.subtitleEditor.subtitleList = newSList;
this.subtitleEditor.translatedList = newTList;
// 同步清空历史标记
this.subtitleEditor.clearedSIndices = [];
this.subtitleEditor.clearedTIndices = [];
this.$message.success('修改已应用到内存');
},
......@@ -1350,6 +1352,73 @@ function initVueApp(savedData) {
return NaN;
},
// 侦测原始时间格式(基于数组首个有效项的 start/end)
detectTimeFormatFromArray(arr) {
const sample = (arr || []).find(x => x && (x.start != null || x.end != null)) || {};
const v = sample.start != null ? sample.start : sample.end;
if (v == null) {
// 默认使用字符串时码 MM:SS.mmm
return { kind: 'string', unit: 'clock', segments: 2, decimals: 3, decimalSeparator: '.', };
}
if (typeof v === 'number') {
if (Number.isInteger(v) && v >= 1000) {
return { kind: 'number', unit: 'ms' };
}
// 秒(可能是小数)
const decimals = (v.toString().split('.')[1] || '').length;
return { kind: 'number', unit: 'sec', decimals: Math.min(Math.max(decimals, 0), 3) };
}
if (typeof v === 'string') {
const hasComma = v.indexOf(',') >= 0;
const decSep = hasComma ? ',' : '.';
if (v.includes(':')) {
const segs = v.split(':').length;
const m = v.match(/[.,](\d{1,3})\s*$/);
const decimals = m ? m[1].length : 0;
return { kind: 'string', unit: 'clock', segments: segs >= 3 ? 3 : 2, decimals: decimals || 0, decimalSeparator: decSep };
} else {
// 纯秒字符串
const m = v.match(/[.,](\d{1,3})\s*$/);
const decimals = m ? m[1].length : 0;
return { kind: 'string', unit: 'sec', decimals: decimals || 0, decimalSeparator: decSep };
}
}
// 兜底
return { kind: 'string', unit: 'clock', segments: 2, decimals: 3, decimalSeparator: '.', };
},
// 将秒数序列化回原始格式
serializeTimeByFormat(seconds, fmt) {
if (seconds == null || isNaN(seconds)) return seconds;
const f = fmt || { kind: 'string', unit: 'clock', segments: 2, decimals: 3, decimalSeparator: '.' };
if (f.kind === 'number') {
if (f.unit === 'ms') return Math.round(seconds * 1000);
const dec = typeof f.decimals === 'number' ? f.decimals : 3;
const val = Number(seconds.toFixed(dec));
return val;
}
// 字符串
const decSep = f.decimalSeparator || '.';
const decimals = typeof f.decimals === 'number' ? f.decimals : 3;
const pad = n => n.toString().padStart(2, '0');
const msInt = Math.round((seconds % 1) * Math.pow(10, decimals));
if (f.unit === 'clock') {
let s = Math.floor(seconds);
const h = Math.floor(s / 3600); s = s % 3600;
const m = Math.floor(s / 60);
const sec = s % 60;
const frac = decimals > 0 ? (decSep + msInt.toString().padStart(decimals, '0')) : '';
if (f.segments === 3 || h > 0) {
return `${pad(h)}:${pad(m)}:${pad(sec)}${frac}`;
}
return `${pad(m)}:${pad(sec)}${frac}`;
}
// 纯秒字符串
const intPart = Math.floor(seconds);
const frac = decimals > 0 ? (decSep + Math.round((seconds - intPart) * Math.pow(10, decimals)).toString().padStart(decimals, '0')) : '';
return `${intPart}${frac}`;
},
// 工具:安全 JSON.stringify
safeStringify(obj) {
try {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment