This commit is contained in:
xunbu committed 2025-08-05 00:10:46 +08:00
1 parent 262abe88c6
commit fb99ed78e3
5 files changed
+72 -46

No files matched your search

+29 -8
View File
@@ -1,4 +1,5 @@
import json
from json_repair import json_repair
from dataclasses import dataclass
from json import JSONDecodeError
@@ -28,12 +29,18 @@ class SegmentsTranslateAgent(Agent):
翻译后的片段应该与源格式尽量相同
如果待翻译片段已经是目标语言,则保持原样
# 输出
翻译后的片段序列,以json文本表示(文本而非代码块)。其中键是片段编号,值是翻译后的片段
翻译后的片段序列,以json文本表示(注意不是代码块)。其中键是片段编号,值是翻译后的片段。
返回的json文本必须能被json.loads转换为形如{{"片段编号":"译文"}}的字典。
# 示例
## 输入
{r'{"0":"hello","1":"apple","2":true,"3":"false"}'}
## 输出
{r'{"0":"你好","1":"苹果","2":true,"3":"错误"}'}
# 错误输出示例
"{r'"{\"0\":\"你好\",\"1\":\"苹果\"}"'}"
# 正确输出示例
{r'{"0":"你好","1":"苹果"}'}
警告:绝不要将整个JSON对象用引号包裹成一个字符串。
"""
if config.custom_prompt:
self.system_prompt += "\n# 重要规则或背景【非常重要】\n" + config.custom_prompt + '\n'
@@ -41,26 +48,40 @@ class SegmentsTranslateAgent(Agent):
def send_segments(self, segments: list[str], chunk_size: int):
indexed_originals = {str(i): text for i, text in enumerate(segments)}
chunks = flat_json_split(indexed_originals, chunk_size)
prompts = [json.dumps(chunk) for chunk in chunks]
prompts = [json.dumps(chunk,ensure_ascii=False) for chunk in chunks]
translated_chunks = super().send_prompts(prompts=prompts)
indexed_translated = indexed_originals.copy()
for chunk_str in translated_chunks:
translated_part = json.loads(chunk_str)
indexed_translated.update(translated_part)
try:
translated_part = json_repair.loads(chunk_str)
for key,val in translated_part:
if key in indexed_translated:
indexed_translated[key]=val
except JSONDecodeError as e:
self.logger.info(f"json解析错误,解析文本:{chunk_str},错误:{e.__repr__()}")
except ValueError as e:
self.logger.info(f"value错误,更新对象:{indexed_translated},错误:{e.__repr__()}")
return list(indexed_translated.values())
#todo:增加协程粒度
async def send_segments_async(self, segments: list[str], chunk_size: int):
indexed_originals = {str(i): text for i, text in enumerate(segments)}
chunks = flat_json_split(indexed_originals, chunk_size)
prompts = [json.dumps(chunk) for chunk in chunks]
prompts = [json.dumps(chunk,ensure_ascii=False) for chunk in chunks]
translated_chunks = await super().send_prompts_async(prompts=prompts)
indexed_translated = indexed_originals.copy()
for chunk_str in translated_chunks:
try:
translated_part = json.loads(chunk_str)
indexed_translated.update(translated_part)
translated_part:dict = json_repair.loads(chunk_str)
for key,val in translated_part.items():
if key in indexed_translated:
indexed_translated[key]=val
except JSONDecodeError as e:
self.logger.info(f"json解析错误,解析文本:{chunk_str},错误:{e.__repr__()}")
self.logger.error(f"json解析错误,解析文本:{chunk_str},错误:{e.__repr__()}")
except ValueError as e:
self.logger.error(f"value错误,更新对象:{indexed_translated},错误:{e.__repr__()}")
except AttributeError as e:
self.logger.error(f"属性错误,chunk_str:{chunk_str},错误:{e.__repr__()}")
return list(indexed_translated.values())
@@ -3,6 +3,7 @@ from io import BytesIO
import jinja2
import openpyxl
from xlsx2html import xlsx2html
from docutranslate.exporter.base import ExporterConfig
from docutranslate.exporter.xlsx.base import XlsxExporter
@@ -22,40 +23,5 @@ class Xlsx2HTMLExporter(XlsxExporter):
self.cdn = config.cdn
def export(self, document: Document) -> Document:
# 1. 加载工作簿和工作表
workbook = openpyxl.load_workbook(BytesIO(document.content))
sheet = workbook.active
# 2. 手动构建HTML字符串
table = '<table border="1">\n'
# 处理表头
table += ' <thead>\n <tr>\n'
for cell in sheet[1]: # 假设第一行是表头
table += f' <th>{cell.value}</th>\n'
table += ' </tr>\n </thead>\n'
# 处理数据行
table += ' <tbody>\n'
# iter_rows(min_row=2) 从第二行开始遍历
for row in sheet.iter_rows(min_row=2):
table += ' <tr>\n'
for cell in row:
# 处理None值,防止在HTML中显示"None"
cell_value = cell.value if cell.value is not None else ""
table += f' <td>{cell_value}</td>\n'
table += ' </tr>\n'
table += ' </tbody>\n'
table += '</table>'
html_template = resource_path("template/xlsx.html").read_text(encoding="utf-8")
pico = f'<style>{resource_path("static/pico.css").read_text(encoding="utf-8")}</style>' if not self.cdn else r'<link rel="stylesheet" href="https://s4.zstatic.net/ajax/libs/picocss/2.1.1/pico.min.css" integrity="sha512-+4kjFgVD0n6H3xt19Ox84B56MoS7srFn60tgdWFuO4hemtjhySKyW4LnftYZn46k3THUEiTTsbVjrHai+0MOFw==" crossorigin="anonymous" referrerpolicy="no-referrer" />'
render = jinja2.Template(html_template).render(
title=document.stem,
pico=pico,
body=table,
)
return Document.from_bytes(content=render.encode("utf-8"), suffix=".html", stem=document.stem)
html_content = xlsx2html(BytesIO(document.content), output=None).getvalue()
return Document.from_bytes(content=html_content.encode("utf-8"), suffix=".html", stem=document.stem)
+1 -1
View File
@@ -1 +1 @@
<!DOCTYPE html> <html lang="zh-CN" data-bs-theme="auto"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>DocuTranslate - 交互式文档翻译</title> <link rel="icon" href="/static/favicon.ico" type="image/x-icon"> <!-- Bootstrap CSS --> <link href="/static/bootstrap.css" rel="stylesheet" crossorigin="anonymous"> <!-- Bootstrap Icons --> <link rel="stylesheet" href="/static/bootstrap-icons.css"> <style> body { background-color: var(--bs-body-bg); } .main-container { display: flex; flex-direction: column; height: 100vh; padding-top: 1rem; padding-bottom: 1rem; } .settings-panel { height: calc(100vh - 2rem); overflow-y: auto; padding-right: 15px; /* for scrollbar */ } .task-area { height: calc(100vh - 2rem); overflow-y: auto; } .task-card { transition: all 0.3s ease-in-out; } .task-id-placeholder { color: var(--bs-secondary-color); font-style: italic; } .log-area { height: 150px; background-color: var(--bs-tertiary-bg); border: 1px solid var(--bs-border-color); border-radius: .375rem; font-family: monospace; font-size: 0.8rem; padding: 10px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; } .file-drop-area { border: 2px dashed var(--bs-secondary-bg); border-radius: .375rem; padding: 2rem; text-align: center; cursor: pointer; transition: all 0.2s ease-in-out; background-color: var(--bs-body-bg); } .file-drop-area.drag-over { border-color: var(--bs-primary); background-color: var(--bs-secondary-bg); } .file-drop-area.file-selected { border-style: solid; border-color: var(--bs-success); background-color: var(--bs-success-bg-subtle); } .file-drop-area.input-error { border-color: var(--bs-danger); } .file-name-display.input-error-text { color: var(--bs-danger); font-weight: bold; } #printFrame, #translatedPreviewFrame { border: none; width: 100%; } #previewOffcanvas { --bs-offcanvas-width: 95vw; max-width: 1600px; } .preview-split-container { display: flex; flex-direction: row; height: 100%; } .preview-pane-wrapper { display: flex; flex-direction: column; overflow: hidden; /* Important for split.js */ } .preview-pane-wrapper h6 { flex-shrink: 0; padding: 0.25rem; } .preview-pane-wrapper .preview-pane { flex-grow: 1; /* Make the inner pane grow */ border: 1px solid var(--bs-border-color); border-radius: .375rem; overflow: auto; } .gutter { background-color: var(--bs-tertiary-bg); background-repeat: no-repeat; background-position: 50%; } .gutter.gutter-horizontal { cursor: col-resize; border-left: 1px solid var(--bs-border-color); border-right: 1px solid var(--bs-border-color); } .gutter.gutter-vertical { cursor: row-resize; border-top: 1px solid var(--bs-border-color); border-bottom: 1px solid var(--bs-border-color); } .preview-pane iframe, .preview-pane pre { width: 100%; height: 95%; border: none; margin: 0; padding: 0; overflow: auto; background-color: var(--bs-body-bg); } .slider-reset-btn { visibility: hidden; } .theme-switch { position: fixed; bottom: 1rem; left: 1rem; z-index: 1050; } @media (max-width: 991.98px) { .main-container { height: auto; padding-bottom: 6rem; } .settings-panel, .task-area { height: auto; overflow-y: visible; } .settings-panel { padding-right: 0; margin-bottom: 2rem; } } @media (max-width: 767.98px) { .task-card .col-md-7 { margin-top: 1.5rem; } #previewOffcanvas { --bs-offcanvas-width: 100vw; } Line truncated
<!DOCTYPE html> <html lang="zh-CN" data-bs-theme="auto"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>DocuTranslate - 交互式文档翻译</title> <link rel="icon" href="/static/favicon.ico" type="image/x-icon"> <!-- Bootstrap CSS --> <link href="/static/bootstrap.css" rel="stylesheet" crossorigin="anonymous"> <!-- Bootstrap Icons --> <link rel="stylesheet" href="/static/bootstrap-icons.css"> <style> body { background-color: var(--bs-body-bg); } .main-container { display: flex; flex-direction: column; height: 100vh; padding-top: 1rem; padding-bottom: 1rem; } .settings-panel { height: calc(100vh - 2rem); overflow-y: auto; padding-right: 15px; /* for scrollbar */ } .task-area { height: calc(100vh - 2rem); overflow-y: auto; } .task-card { transition: all 0.3s ease-in-out; } .task-id-placeholder { color: var(--bs-secondary-color); font-style: italic; } .log-area { height: 150px; background-color: var(--bs-tertiary-bg); border: 1px solid var(--bs-border-color); border-radius: .375rem; font-family: monospace; font-size: 0.8rem; padding: 10px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; } .file-drop-area { border: 2px dashed var(--bs-secondary-bg); border-radius: .375rem; padding: 2rem; text-align: center; cursor: pointer; transition: all 0.2s ease-in-out; background-color: var(--bs-body-bg); } .file-drop-area.drag-over { border-color: var(--bs-primary); background-color: var(--bs-secondary-bg); } .file-drop-area.file-selected { border-style: solid; border-color: var(--bs-success); background-color: var(--bs-success-bg-subtle); } .file-drop-area.input-error { border-color: var(--bs-danger); } .file-name-display.input-error-text { color: var(--bs-danger); font-weight: bold; } #printFrame, #translatedPreviewFrame { border: none; width: 100%; } #previewOffcanvas { --bs-offcanvas-width: 95vw; max-width: 1600px; } .preview-split-container { display: flex; flex-direction: row; height: 100%; } .preview-pane-wrapper { display: flex; flex-direction: column; overflow: hidden; /* Important for split.js */ } .preview-pane-wrapper h6 { flex-shrink: 0; padding: 0.25rem; } .preview-pane-wrapper .preview-pane { flex-grow: 1; /* Make the inner pane grow */ border: 1px solid var(--bs-border-color); border-radius: .375rem; overflow: auto; } .gutter { background-color: var(--bs-tertiary-bg); background-repeat: no-repeat; background-position: 50%; } .gutter.gutter-horizontal { cursor: col-resize; border-left: 1px solid var(--bs-border-color); border-right: 1px solid var(--bs-border-color); } .gutter.gutter-vertical { cursor: row-resize; border-top: 1px solid var(--bs-border-color); border-bottom: 1px solid var(--bs-border-color); } .preview-pane iframe, .preview-pane pre { width: 100%; height: 95%; border: none; margin: 0; padding: 0; overflow: auto; background-color: var(--bs-body-bg); } .slider-reset-btn { visibility: hidden; } .theme-switch { position: fixed; bottom: 1rem; left: 1rem; z-index: 1050; } @media (max-width: 991.98px) { .main-container { height: auto; padding-bottom: 6rem; } .settings-panel, .task-area { height: auto; overflow-y: visible; } .settings-panel { padding-right: 0; margin-bottom: 2rem; } } @media (max-width: 767.98px) { .task-card .col-md-7 { margin-top: 1.5rem; } #previewOffcanvas { --bs-offcanvas-width: 100vw; } Line truncated