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:
@@ -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"],
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user