repaire sentences
This commit is contained in:
@@ -81,7 +81,7 @@ table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #e3e7ea; vertical-align: middle; }
|
||||
th { position: sticky; top: 0; z-index: 1; color: #44515b; background: #edf1f3; font-weight: 600; }
|
||||
tr:last-child td { border-bottom: 0; }
|
||||
.actions-column { min-width: 280px; }
|
||||
.actions-column { min-width: 340px; }
|
||||
.actions { display: flex; gap: 6px; white-space: nowrap; }
|
||||
.actions button, .actions a { min-height: 30px; padding: 5px 9px; font-size: 12px; }
|
||||
.actions a { display: inline-flex; align-items: center; border: 1px solid #aab4bc; border-radius: 4px; color: #26323c; text-decoration: none; }
|
||||
@@ -103,6 +103,8 @@ dialog {
|
||||
dialog::backdrop { background: rgb(20 25 28 / 55%); }
|
||||
.dialog-heading { padding: 16px 18px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #dce1e5; }
|
||||
.dialog-heading span { font-size: 12px; color: #68747d; }
|
||||
.dialog-heading-actions { display: flex; align-items: center; gap: 8px; }
|
||||
.gap-row td { background: #fff8ee; color: #9a5b12; font-style: italic; }
|
||||
.sentence-scroll { max-height: calc(100vh - 120px); overflow: auto; }
|
||||
.sentence-scroll textarea { width: min(600px, 45vw); min-width: 260px; min-height: 58px; resize: vertical; padding: 7px; border: 1px solid #b8c0c7; border-radius: 3px; }
|
||||
.sentence-scroll select { min-width: 90px; }
|
||||
|
||||
@@ -122,7 +122,10 @@
|
||||
<h2 id="sentence-title">句子文本</h2>
|
||||
<span id="sentence-meta"></span>
|
||||
</div>
|
||||
<button id="close-dialog" class="icon-button" type="button" aria-label="关闭">X</button>
|
||||
<div class="dialog-heading-actions">
|
||||
<button id="repair-gaps" type="button">修复空白</button>
|
||||
<button id="close-dialog" class="icon-button" type="button" aria-label="关闭">X</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sentence-scroll">
|
||||
<table>
|
||||
@@ -134,6 +137,6 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script src="/static/admin.js?v=admin-retranslate-v1" defer></script>
|
||||
<script src="/static/admin.js?v=admin-repair-gaps-v1" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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