fix: persist translated_filename_stem and pass it to printPdf

- _translate_filename_stem: add success/failure logging, include
  glossary terms in prompt for accurate proper noun translation
- task_state: store translated_filename_stem after translation
- status API: return translated_filename_stem to frontend
- printPdf: accept stem param, set document.title directly instead
  of fragile Content-Disposition header parsing
- Both printPdf call sites now pass task.translatedStem
This commit is contained in:
2026-07-13 18:31:23 +08:00
parent b4538801ea
commit 4045d7faf2
2 changed files with 35 additions and 29 deletions

View File

@@ -175,6 +175,7 @@ def _create_default_task_state() -> Dict[str, Any]:
"download_ready": False, "download_ready": False,
"workflow_instance": None, # 仅在处理期间使用 "workflow_instance": None, # 仅在处理期间使用
"original_filename_stem": None, "original_filename_stem": None,
"translated_filename_stem": None,
"task_start_time": 0, "task_start_time": 0,
"task_end_time": 0, "task_end_time": 0,
"current_task_ref": None, "current_task_ref": None,
@@ -631,11 +632,31 @@ async def _translate_filename_stem(
stem: str, to_lang: str, base_url: str | None, api_key: str, model_id: str | None stem: str, to_lang: str, base_url: str | None, api_key: str, model_id: str | None
) -> str: ) -> str:
"""Translate a filename stem to target language for cleaner download filenames.""" """Translate a filename stem to target language for cleaner download filenames."""
_logger = logging.getLogger(__name__)
try: try:
clean_base = (base_url or "").strip().rstrip("/") clean_base = (base_url or "").strip().rstrip("/")
clean_model = (model_id or "").strip() clean_model = (model_id or "").strip()
if not clean_base or not clean_model: if not clean_base or not clean_model:
_logger.warning(f"Filename translation skipped: LLM not configured (stem='{stem}')")
return stem return stem
# Include glossary terms so proper nouns translate correctly
try:
from docutranslate.glossary.glossary_store import get_glossary_store
rows = get_glossary_store().get_rows()
glossary_hint = ", ".join(
f"{r.get('zh', '')}={r.get('en', '')}"
for r in rows[:20]
if r.get('zh') and r.get('en')
)
except Exception:
glossary_hint = ""
prompt = (
f"Translate the document filename below into {to_lang}. "
"Return ONLY the translated name, no file extension, no explanation, no quotes.\n"
)
if glossary_hint:
prompt += f"Reference glossary if relevant: {glossary_hint}\n"
prompt += f"Filename: {stem}"
resp = await httpx_client.post( resp = await httpx_client.post(
f"{clean_base}/chat/completions", f"{clean_base}/chat/completions",
headers={ headers={
@@ -644,16 +665,7 @@ async def _translate_filename_stem(
}, },
json={ json={
"model": clean_model, "model": clean_model,
"messages": [ "messages": [{"role": "user", "content": prompt}],
{
"role": "user",
"content": (
f"Translate the document filename below into {to_lang}. "
"Return ONLY the translated name, no file extension, no explanation, no quotes.\n"
f"Filename: {stem}"
),
}
],
"max_tokens": 60, "max_tokens": 60,
"temperature": 0, "temperature": 0,
}, },
@@ -663,8 +675,11 @@ async def _translate_filename_stem(
translated = resp.json()["choices"][0]["message"]["content"].strip().strip("\"'") translated = resp.json()["choices"][0]["message"]["content"].strip().strip("\"'")
sanitized = re.sub(r"[^\w\s\-]", "", translated).strip() sanitized = re.sub(r"[^\w\s\-]", "", translated).strip()
sanitized = re.sub(r"\s+", "_", sanitized)[:50] sanitized = re.sub(r"\s+", "_", sanitized)[:50]
return sanitized if sanitized else stem result = sanitized if sanitized else stem
except Exception: _logger.info(f"Filename translation: '{stem}' -> '{result}' (lang={to_lang})")
return result
except Exception as e:
_logger.warning(f"Filename translation failed for '{stem}': {e}")
return stem return stem
@@ -1178,6 +1193,7 @@ async def _perform_translation(
getattr(payload, "base_url", None), getattr(payload, "api_key", ""), getattr(payload, "base_url", None), getattr(payload, "api_key", ""),
getattr(payload, "model_id", None), getattr(payload, "model_id", None),
) )
task_state["translated_filename_stem"] = translated_stem
# 检查CDN可用性 # 检查CDN可用性
is_cdn_available = True is_cdn_available = True
@@ -1972,6 +1988,7 @@ async def service_get_status(
"error_flag": task_state["error_flag"], "error_flag": task_state["error_flag"],
"download_ready": task_state["download_ready"], "download_ready": task_state["download_ready"],
"original_filename_stem": task_state["original_filename_stem"], "original_filename_stem": task_state["original_filename_stem"],
"translated_filename_stem": task_state.get("translated_filename_stem") or task_state["original_filename_stem"],
"original_filename": task_state.get("original_filename"), "original_filename": task_state.get("original_filename"),
"task_start_time": task_state["task_start_time"], "task_start_time": task_state["task_start_time"],
"task_end_time": task_state["task_end_time"], "task_end_time": task_state["task_end_time"],

View File

@@ -807,7 +807,7 @@
</li> </li>
<li v-if="task.downloads.html && !task.downloads.pptx"> <li v-if="task.downloads.html && !task.downloads.pptx">
<a class="dropdown-item" href="#" <a class="dropdown-item" href="#"
@click.prevent="printPdf(task.downloads.html)"><i @click.prevent="printPdf(task.downloads.html, task.translatedStem)"><i
class="bi bi-file-earmark-pdf me-2"></i>PDF</a> class="bi bi-file-earmark-pdf me-2"></i>PDF</a>
</li> </li>
</ul> </ul>
@@ -933,7 +933,7 @@
</a> </a>
</li> </li>
<li v-if="previewTask.downloads.html && !previewTask.downloads.pptx"> <li v-if="previewTask.downloads.html && !previewTask.downloads.pptx">
<a class="dropdown-item" href="#" @click.prevent="printPdf(previewTask.downloads.html)"> <a class="dropdown-item" href="#" @click.prevent="printPdf(previewTask.downloads.html, previewTask.translatedStem)">
<i class="bi bi-file-earmark-pdf me-2"></i>PDF <i class="bi bi-file-earmark-pdf me-2"></i>PDF
</a> </a>
</li> </li>
@@ -1872,6 +1872,7 @@
if (statData.download_ready && !statData.error_flag) { if (statData.download_ready && !statData.error_flag) {
task.downloads = statData.downloads; task.downloads = statData.downloads;
task.attachment = statData.attachment; task.attachment = statData.attachment;
task.translatedStem = statData.translated_filename_stem || statData.original_filename_stem;
task.statusClass = 'text-success'; task.statusClass = 'text-success';
} else { } else {
task.statusClass = 'text-danger'; task.statusClass = 'text-danger';
@@ -1978,7 +1979,7 @@
localStorage.setItem('ui_sync_scroll_enabled', syncScrollEnabled.value); localStorage.setItem('ui_sync_scroll_enabled', syncScrollEnabled.value);
}; };
const printPdf = (url) => { const printPdf = (url, stem) => {
const msg = t('pdf_preparing') || "正在准备打印,请稍候..."; const msg = t('pdf_preparing') || "正在准备打印,请稍候...";
const toastContainer = document.createElement('div'); const toastContainer = document.createElement('div');
toastContainer.className = 'toast-container position-fixed top-0 start-50 translate-middle-x p-3'; toastContainer.className = 'toast-container position-fixed top-0 start-50 translate-middle-x p-3';
@@ -2005,25 +2006,13 @@
}, 3000); }, 3000);
const ifr = document.getElementById('printFrame'); const ifr = document.getElementById('printFrame');
fetch(url).then(r => { fetch(url).then(r => r.text()).then(h => {
const cd = r.headers.get('content-disposition') || '';
// RFC 5987: filename*=UTF-8''<percent-encoded>
const extMatch = cd.match(/filename\*\s*=\s*UTF-8''([^;\s]+)/i);
let printTitle = '';
if (extMatch) {
try { printTitle = decodeURIComponent(extMatch[1]).replace(/\.[^.]+$/, ''); } catch(e) {}
} else {
const plainMatch = cd.match(/filename\s*=\s*["']?([^"'\n;]+)["']?/i);
if (plainMatch) printTitle = plainMatch[1].trim().replace(/\.[^.]+$/, '');
}
return r.text().then(h => ({ h, printTitle }));
}).then(({ h, printTitle }) => {
ifr.srcdoc = h; ifr.srcdoc = h;
ifr.onload = () => { ifr.onload = () => {
setTimeout(() => { setTimeout(() => {
ifr.contentWindow.focus(); ifr.contentWindow.focus();
const originalTitle = document.title; const originalTitle = document.title;
if (printTitle) document.title = printTitle; if (stem) document.title = stem;
ifr.contentWindow.print(); ifr.contentWindow.print();
setTimeout(() => { document.title = originalTitle; }, 1000); setTimeout(() => { document.title = originalTitle; }, 1000);
}, 500); }, 500);