add a retranslate chinese function
This commit is contained in:
@@ -119,3 +119,11 @@ dialog::backdrop { background: rgb(20 25 28 / 55%); }
|
||||
#upload-form { grid-template-columns: 1fr; }
|
||||
.actions-column { min-width: 250px; }
|
||||
}
|
||||
|
||||
.zh-text { margin-top: 6px; color: #66727d; font-size: 12px; white-space: pre-wrap; line-height: 1.45; }
|
||||
.retranslate-form { padding: 16px 18px 20px; display: grid; gap: 12px; }
|
||||
.retranslate-form p { margin: 0; font-size: 13px; color: #4f5b64; line-height: 1.55; }
|
||||
.retranslate-form label { display: grid; gap: 6px; font-size: 13px; color: #4f5b64; }
|
||||
.retranslate-form textarea { width: 100%; min-height: 160px; resize: vertical; }
|
||||
.retranslate-form code { font-size: 12px; }
|
||||
.retranslate-actions { display: flex; justify-content: flex-end; }
|
||||
|
||||
@@ -96,6 +96,26 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<dialog id="retranslate-dialog">
|
||||
<div class="dialog-heading">
|
||||
<div>
|
||||
<h2 id="retranslate-title">重新翻译中文字幕</h2>
|
||||
<span id="retranslate-meta"></span>
|
||||
</div>
|
||||
<button id="close-retranslate-dialog" class="icon-button" type="button" aria-label="关闭">X</button>
|
||||
</div>
|
||||
<form id="retranslate-form" class="retranslate-form">
|
||||
<p>会清空当前中文字幕并重新翻译。可指定专有名词译法,每行一条,格式为 <code>原文=中文</code>。例如 <code>Harimoto=哈瑞么托</code>,也可以把错误译名写在左边:<code>哈达马托=哈瑞么托</code>。</p>
|
||||
<label>
|
||||
<span>专有名词译法</span>
|
||||
<textarea id="retranslate-terms" placeholder="Harimoto=哈瑞么托"></textarea>
|
||||
</label>
|
||||
<div class="retranslate-actions">
|
||||
<button class="primary" type="submit">开始重新翻译</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="sentence-dialog">
|
||||
<div class="dialog-heading">
|
||||
<div>
|
||||
@@ -114,6 +134,6 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script src="/static/admin.js?v=admin-course-title-v1" defer></script>
|
||||
<script src="/static/admin.js?v=admin-retranslate-v1" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,6 +2,7 @@ const state = {
|
||||
adminKey: localStorage.getItem("oralTrainerAdminKey") || "",
|
||||
videos: [],
|
||||
users: [],
|
||||
retranslateVideo: null,
|
||||
};
|
||||
|
||||
const elements = {
|
||||
@@ -22,6 +23,12 @@ const elements = {
|
||||
sentenceMeta: document.querySelector("#sentence-meta"),
|
||||
sentenceRows: document.querySelector("#sentence-rows"),
|
||||
closeDialog: document.querySelector("#close-dialog"),
|
||||
retranslateDialog: document.querySelector("#retranslate-dialog"),
|
||||
retranslateTitle: document.querySelector("#retranslate-title"),
|
||||
retranslateMeta: document.querySelector("#retranslate-meta"),
|
||||
retranslateForm: document.querySelector("#retranslate-form"),
|
||||
retranslateTerms: document.querySelector("#retranslate-terms"),
|
||||
closeRetranslateDialog: document.querySelector("#close-retranslate-dialog"),
|
||||
userRows: document.querySelector("#user-rows"),
|
||||
userCount: document.querySelector("#user-count"),
|
||||
userEmptyState: document.querySelector("#user-empty-state"),
|
||||
@@ -38,6 +45,8 @@ elements.refresh.addEventListener("click", () => {
|
||||
loadUsers();
|
||||
});
|
||||
elements.closeDialog.addEventListener("click", () => elements.dialog.close());
|
||||
elements.closeRetranslateDialog.addEventListener("click", () => elements.retranslateDialog.close());
|
||||
elements.retranslateForm.addEventListener("submit", submitRetranslate);
|
||||
elements.uploadForm.addEventListener("submit", uploadVideo);
|
||||
elements.videoFile.addEventListener("change", () => {
|
||||
if (!elements.videoFile.files[0]) return;
|
||||
@@ -183,6 +192,7 @@ function actionCell(video) {
|
||||
actions.append(play);
|
||||
actions.append(button("改标题", () => renameVideo(video)));
|
||||
actions.append(button("句子", () => openSentences(video)));
|
||||
actions.append(button("重新翻译", () => openRetranslate(video)));
|
||||
actions.append(button("重新处理", () => reprocess(video.video_hash)));
|
||||
const remove = button("删除", () => deleteVideo(video));
|
||||
remove.className = "danger";
|
||||
@@ -298,7 +308,10 @@ function sentenceRow(video, sentence, totalCount) {
|
||||
language.append(option);
|
||||
}
|
||||
const textContainer = document.createElement("td");
|
||||
textContainer.append(textArea);
|
||||
const zhNote = document.createElement("div");
|
||||
zhNote.className = "zh-text";
|
||||
zhNote.textContent = sentence.zh_text || "暂无中文翻译";
|
||||
textContainer.append(textArea, zhNote);
|
||||
const languageContainer = document.createElement("td");
|
||||
languageContainer.append(language);
|
||||
const saveContainer = document.createElement("td");
|
||||
@@ -408,6 +421,75 @@ function boundaryControl(video, sentence) {
|
||||
return container;
|
||||
}
|
||||
|
||||
async function openRetranslate(video) {
|
||||
state.retranslateVideo = video;
|
||||
elements.retranslateTitle.textContent = `重新翻译:${video.title}`;
|
||||
elements.retranslateMeta.textContent = video.video_hash;
|
||||
elements.retranslateTerms.value = "";
|
||||
try {
|
||||
const payload = await api(`/api/v1/admin/videos/${video.video_hash}/subtitle-translation`);
|
||||
elements.retranslateTerms.value = formatTranslationTerms(payload.terms || []);
|
||||
const status = payload.complete ? "中文字幕已就绪" : (payload.status === "running" ? "正在翻译" : "尚未翻译完成");
|
||||
elements.retranslateMeta.textContent = `${status} / ${video.video_hash}`;
|
||||
} catch (error) {
|
||||
elements.serviceState.textContent = `加载翻译信息失败: ${error.message}`;
|
||||
}
|
||||
if (!elements.retranslateDialog.open) elements.retranslateDialog.showModal();
|
||||
}
|
||||
|
||||
function formatTranslationTerms(terms) {
|
||||
return (terms || []).map((item) => `${item.source}=${item.zh}`).join("\n");
|
||||
}
|
||||
|
||||
function parseTranslationTerms(text) {
|
||||
const terms = [];
|
||||
for (const rawLine of (text || "").split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
const match = line.match(/^(.+?)\s*(?:=|=>|:|:)\s*(.+)$/);
|
||||
if (!match) {
|
||||
throw new Error(`无法解析专有名词:${line}`);
|
||||
}
|
||||
const source = match[1].trim();
|
||||
const zh = match[2].trim();
|
||||
if (!source || !zh) {
|
||||
throw new Error(`无法解析专有名词:${line}`);
|
||||
}
|
||||
terms.push({ source, zh });
|
||||
}
|
||||
return terms;
|
||||
}
|
||||
|
||||
async function submitRetranslate(event) {
|
||||
event.preventDefault();
|
||||
const video = state.retranslateVideo;
|
||||
if (!video) return;
|
||||
let terms;
|
||||
try {
|
||||
terms = parseTranslationTerms(elements.retranslateTerms.value);
|
||||
} catch (error) {
|
||||
elements.serviceState.textContent = error.message;
|
||||
return;
|
||||
}
|
||||
const submit = elements.retranslateForm.querySelector("button[type='submit']");
|
||||
submit.disabled = true;
|
||||
try {
|
||||
const payload = await api(`/api/v1/admin/videos/${video.video_hash}/subtitle-translation`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ terms }),
|
||||
});
|
||||
const status = payload.complete ? "中文字幕已重新翻译" : "已开始重新翻译";
|
||||
elements.serviceState.textContent = terms.length ? `${status},已应用 ${terms.length} 条专有名词` : status;
|
||||
elements.retranslateDialog.close();
|
||||
await loadVideos();
|
||||
} catch (error) {
|
||||
elements.serviceState.textContent = `重新翻译失败: ${error.message}`;
|
||||
} finally {
|
||||
submit.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reprocess(videoHash) {
|
||||
try {
|
||||
await api(`/api/v1/admin/videos/${videoHash}/process`, { method: "POST" });
|
||||
|
||||
Reference in New Issue
Block a user