repaire sentences
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
const GAP_REPAIR_MIN_MS = 8000;
|
||||
|
||||
const state = {
|
||||
adminKey: localStorage.getItem("oralTrainerAdminKey") || "",
|
||||
videos: [],
|
||||
users: [],
|
||||
retranslateVideo: null,
|
||||
sentenceVideo: null,
|
||||
};
|
||||
|
||||
const elements = {
|
||||
@@ -23,6 +26,7 @@ const elements = {
|
||||
sentenceMeta: document.querySelector("#sentence-meta"),
|
||||
sentenceRows: document.querySelector("#sentence-rows"),
|
||||
closeDialog: document.querySelector("#close-dialog"),
|
||||
repairGaps: document.querySelector("#repair-gaps"),
|
||||
retranslateDialog: document.querySelector("#retranslate-dialog"),
|
||||
retranslateTitle: document.querySelector("#retranslate-title"),
|
||||
retranslateMeta: document.querySelector("#retranslate-meta"),
|
||||
@@ -45,6 +49,9 @@ elements.refresh.addEventListener("click", () => {
|
||||
loadUsers();
|
||||
});
|
||||
elements.closeDialog.addEventListener("click", () => elements.dialog.close());
|
||||
elements.repairGaps.addEventListener("click", () => {
|
||||
if (state.sentenceVideo) repairGaps(state.sentenceVideo, { reloadSentences: true });
|
||||
});
|
||||
elements.closeRetranslateDialog.addEventListener("click", () => elements.retranslateDialog.close());
|
||||
elements.retranslateForm.addEventListener("submit", submitRetranslate);
|
||||
elements.uploadForm.addEventListener("submit", uploadVideo);
|
||||
@@ -192,6 +199,7 @@ function actionCell(video) {
|
||||
actions.append(play);
|
||||
actions.append(button("改标题", () => renameVideo(video)));
|
||||
actions.append(button("句子", () => openSentences(video)));
|
||||
actions.append(button("修复空白", () => repairGaps(video)));
|
||||
actions.append(button("重新翻译", () => openRetranslate(video)));
|
||||
actions.append(button("重新处理", () => reprocess(video.video_hash)));
|
||||
const remove = button("删除", () => deleteVideo(video));
|
||||
@@ -282,19 +290,61 @@ function readXhrError(request) {
|
||||
async function openSentences(video) {
|
||||
try {
|
||||
const detail = await api(`/api/v1/videos/${video.video_hash}`);
|
||||
state.sentenceVideo = video;
|
||||
elements.sentenceTitle.textContent = detail.video.title;
|
||||
const sentences = detail.boundaries?.sentences || [];
|
||||
elements.sentenceMeta.textContent = `${sentences.length} 句 / ${detail.video.video_hash}`;
|
||||
const durationMs = detail.video.duration_ms || 0;
|
||||
const gaps = collectSentenceGaps(sentences, durationMs);
|
||||
const gapLabel = gaps.length ? ` / ${gaps.length} 段空白待修复` : "";
|
||||
elements.sentenceMeta.textContent = `${sentences.length} 句${gapLabel} / ${detail.video.video_hash}`;
|
||||
elements.repairGaps.disabled = gaps.length === 0;
|
||||
elements.sentenceRows.replaceChildren();
|
||||
let gapIndex = 0;
|
||||
for (const sentence of sentences) {
|
||||
if (gapIndex < gaps.length && gaps[gapIndex].end_ms === sentence.start_ms) {
|
||||
elements.sentenceRows.append(gapRow(gaps[gapIndex]));
|
||||
gapIndex += 1;
|
||||
}
|
||||
elements.sentenceRows.append(sentenceRow(video, sentence, sentences.length));
|
||||
}
|
||||
if (gapIndex < gaps.length) {
|
||||
elements.sentenceRows.append(gapRow(gaps[gapIndex]));
|
||||
}
|
||||
if (!elements.dialog.open) elements.dialog.showModal();
|
||||
} catch (error) {
|
||||
elements.serviceState.textContent = `句子加载失败: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function collectSentenceGaps(sentences, durationMs, minGapMs = GAP_REPAIR_MIN_MS) {
|
||||
const gaps = [];
|
||||
let cursor = 0;
|
||||
for (const sentence of sentences) {
|
||||
if (sentence.start_ms - cursor >= minGapMs) {
|
||||
gaps.push({ start_ms: cursor, end_ms: sentence.start_ms });
|
||||
}
|
||||
cursor = Math.max(cursor, sentence.end_ms);
|
||||
}
|
||||
const endMs = durationMs || cursor;
|
||||
if (endMs - cursor >= minGapMs) {
|
||||
gaps.push({ start_ms: cursor, end_ms: endMs });
|
||||
}
|
||||
return gaps;
|
||||
}
|
||||
|
||||
function gapRow(gap) {
|
||||
const row = document.createElement("tr");
|
||||
row.className = "gap-row";
|
||||
row.append(
|
||||
textCell(""),
|
||||
textCell(`${formatDuration(gap.start_ms)} — ${formatDuration(gap.end_ms)}`),
|
||||
textCell(`未识别空白 ${formatDuration(gap.end_ms - gap.start_ms)}`),
|
||||
textCell(""),
|
||||
textCell(""),
|
||||
);
|
||||
return row;
|
||||
}
|
||||
|
||||
function sentenceRow(video, sentence, totalCount) {
|
||||
const row = document.createElement("tr");
|
||||
const textArea = document.createElement("textarea");
|
||||
@@ -499,6 +549,36 @@ async function reprocess(videoHash) {
|
||||
}
|
||||
}
|
||||
|
||||
async function repairGaps(video, options = {}) {
|
||||
const confirmed = window.confirm(
|
||||
`将对“${video.title}”中间隔超过 8 秒的空白重新识别。现有句子会保留,可能需要几分钟。继续?`,
|
||||
);
|
||||
if (!confirmed) return;
|
||||
const button = elements.repairGaps;
|
||||
if (button) button.disabled = true;
|
||||
elements.serviceState.textContent = "正在重新识别空白段…";
|
||||
try {
|
||||
const payload = await api(`/api/v1/admin/videos/${video.video_hash}/repair-gaps`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ min_gap_ms: GAP_REPAIR_MIN_MS }),
|
||||
});
|
||||
if (payload.inserted_count) {
|
||||
elements.serviceState.textContent = `空白修复完成:补了 ${payload.inserted_count} 句(${payload.transcribed_gaps}/${payload.gap_count} 段)`;
|
||||
} else if (payload.gap_count) {
|
||||
elements.serviceState.textContent = `发现 ${payload.gap_count} 段空白,但没有识别到新句子`;
|
||||
} else {
|
||||
elements.serviceState.textContent = "没有发现需要修复的空白段";
|
||||
}
|
||||
await loadVideos();
|
||||
if (options.reloadSentences) await openSentences(video);
|
||||
} catch (error) {
|
||||
elements.serviceState.textContent = `空白修复失败: ${error.message}`;
|
||||
} finally {
|
||||
if (button && !options.reloadSentences) button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVideo(video) {
|
||||
if (!window.confirm(`删除“${video.title}”?`)) return;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user