feat: translate output filename to target language

Add _translate_filename_stem() which calls the configured LLM to
translate the filename stem before building the export map. Result:
磨粉.pdf translated to English → grinding.pdf (instead of 磨粉_translated.pdf).
Falls back to original stem on any API error with zero impact on translation flow.
This commit is contained in:
2026-07-13 16:13:45 +08:00
parent d86d5c1c64
commit 424d5e01a8

View File

@@ -7,6 +7,7 @@ import binascii
import json import json
import logging import logging
import os import os
import re
import shutil import shutil
import socket import socket
import tempfile import tempfile
@@ -626,6 +627,47 @@ class TranslateServiceRequest(BaseModel):
) )
async def _translate_filename_stem(
stem: str, to_lang: str, base_url: str | None, api_key: str, model_id: str | None
) -> str:
"""Translate a filename stem to target language for cleaner download filenames."""
try:
clean_base = (base_url or "").strip().rstrip("/")
clean_model = (model_id or "").strip()
if not clean_base or not clean_model:
return stem
resp = await httpx_client.post(
f"{clean_base}/chat/completions",
headers={
"Authorization": f"Bearer {(api_key or '').strip()}",
"Content-Type": "application/json",
},
json={
"model": clean_model,
"messages": [
{
"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,
"temperature": 0,
},
timeout=10,
)
resp.raise_for_status()
translated = resp.json()["choices"][0]["message"]["content"].strip().strip("\"'")
sanitized = re.sub(r"[^\w\s\-]", "", translated).strip()
sanitized = re.sub(r"\s+", "_", sanitized)[:50]
return sanitized if sanitized else stem
except Exception:
return stem
# --- Background Task Logic --- # --- Background Task Logic ---
async def _perform_translation( async def _perform_translation(
task_id: str, task_id: str,
@@ -1131,6 +1173,11 @@ async def _perform_translation(
task_state["temp_dir"] = temp_dir task_state["temp_dir"] = temp_dir
downloadable_files = {} downloadable_files = {}
filename_stem = task_state["original_filename_stem"] filename_stem = task_state["original_filename_stem"]
translated_stem = await _translate_filename_stem(
filename_stem, payload.to_lang,
getattr(payload, "base_url", None), getattr(payload, "api_key", ""),
getattr(payload, "model_id", None),
)
# 检查CDN可用性 # 检查CDN可用性
is_cdn_available = True is_cdn_available = True
@@ -1149,66 +1196,66 @@ async def _perform_translation(
if isinstance(workflow, MDFormatsExportable): if isinstance(workflow, MDFormatsExportable):
export_map["markdown"] = ( export_map["markdown"] = (
workflow.export_to_markdown, workflow.export_to_markdown,
f"{filename_stem}_translated.md", f"{translated_stem}.md",
True, True,
) )
export_map["markdown_zip"] = ( export_map["markdown_zip"] = (
workflow.export_to_markdown_zip, workflow.export_to_markdown_zip,
f"{filename_stem}_translated.zip", f"{translated_stem}.zip",
False, False,
) )
if isinstance(workflow, TXTExportable): if isinstance(workflow, TXTExportable):
export_map["txt"] = ( export_map["txt"] = (
workflow.export_to_txt, workflow.export_to_txt,
f"{filename_stem}_translated.txt", f"{translated_stem}.txt",
True, True,
) )
if isinstance(workflow, JsonExportable): if isinstance(workflow, JsonExportable):
export_map["json"] = ( export_map["json"] = (
workflow.export_to_json, workflow.export_to_json,
f"{filename_stem}_translated.json", f"{translated_stem}.json",
True, True,
) )
if isinstance(workflow, XlsxExportable): if isinstance(workflow, XlsxExportable):
export_map["xlsx"] = ( export_map["xlsx"] = (
workflow.export_to_xlsx, workflow.export_to_xlsx,
f"{filename_stem}_translated.xlsx", f"{translated_stem}.xlsx",
False, False,
) )
if isinstance(workflow, CsvExportable): if isinstance(workflow, CsvExportable):
export_map["csv"] = ( export_map["csv"] = (
workflow.export_to_csv, workflow.export_to_csv,
f"{filename_stem}_translated.csv", f"{translated_stem}.csv",
False, False,
) )
if isinstance(workflow, DocxExportable): if isinstance(workflow, DocxExportable):
export_map["docx"] = ( export_map["docx"] = (
workflow.export_to_docx, workflow.export_to_docx,
f"{filename_stem}_translated.docx", f"{translated_stem}.docx",
False, False,
) )
if isinstance(workflow, SrtExportable): if isinstance(workflow, SrtExportable):
export_map["srt"] = ( export_map["srt"] = (
workflow.export_to_srt, workflow.export_to_srt,
f"{filename_stem}_translated.srt", f"{translated_stem}.srt",
True, True,
) )
if isinstance(workflow, EpubExportable): if isinstance(workflow, EpubExportable):
export_map["epub"] = ( export_map["epub"] = (
workflow.export_to_epub, workflow.export_to_epub,
f"{filename_stem}_translated.epub", f"{translated_stem}.epub",
False, False,
) )
if isinstance(workflow, AssExportable): if isinstance(workflow, AssExportable):
export_map["ass"] = ( export_map["ass"] = (
workflow.export_to_ass, workflow.export_to_ass,
f"{filename_stem}_translated.ass", f"{translated_stem}.ass",
True, True,
) )
if isinstance(workflow, PPTXExportable): if isinstance(workflow, PPTXExportable):
export_map["pptx"] = ( export_map["pptx"] = (
workflow.export_to_pptx, workflow.export_to_pptx,
f"{filename_stem}_translated.pptx", f"{translated_stem}.pptx",
False, False,
) )
@@ -1235,7 +1282,7 @@ async def _perform_translation(
html_config = PPTX2HTMLExporterConfig(cdn=is_cdn_available) html_config = PPTX2HTMLExporterConfig(cdn=is_cdn_available)
export_map["html"] = ( export_map["html"] = (
lambda: workflow.export_to_html(html_config), lambda: workflow.export_to_html(html_config),
f"{filename_stem}_translated.html", f"{translated_stem}.html",
True, True,
) )