File size: 8,925 Bytes
fb64c60
 
 
 
 
 
60728b1
 
 
 
d2a17ad
60728b1
 
fb64c60
 
5df6e77
53b6d0e
7f925b0
60728b1
 
 
 
 
d2a17ad
fb64c60
 
 
 
 
 
53b6d0e
 
fb64c60
d70d8b9
53b6d0e
d70d8b9
 
 
 
 
 
fb64c60
 
 
53b6d0e
fb64c60
 
 
 
 
 
 
 
 
5a89e0b
 
fb64c60
 
60728b1
fb64c60
 
 
 
 
 
 
 
 
 
60728b1
 
 
 
 
 
 
d2a17ad
60728b1
 
d2a17ad
60728b1
d2a17ad
60728b1
 
 
d2a17ad
 
60728b1
d2a17ad
60728b1
 
d2a17ad
60728b1
d2a17ad
60728b1
 
 
53b6d0e
 
 
 
 
60728b1
206d124
60728b1
 
 
 
 
 
206d124
fb64c60
206d124
 
 
 
 
53b6d0e
d2a17ad
 
206d124
 
fb64c60
53b6d0e
fb64c60
 
 
206d124
60728b1
fb64c60
 
 
 
 
 
 
 
5a89e0b
 
fb64c60
53b6d0e
60728b1
fb64c60
 
 
 
 
206d124
fb64c60
53b6d0e
fb64c60
 
 
 
d2a17ad
fb64c60
 
206d124
fb64c60
 
 
 
206d124
 
 
d2a17ad
 
fb64c60
53b6d0e
206d124
fb64c60
 
 
 
53b6d0e
 
206d124
 
 
 
 
 
 
 
53b6d0e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# 文件名: processor.py

import os
import sys
import contextlib
from io import StringIO
import json
from pathlib import Path
from datetime import datetime
import re
import time
import traceback

import streamlit as st
import ebooklib
import torch
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM

from book_maker.loader import BOOK_LOADER_DICT
from book_maker.translator import MODEL_DICT
from book_maker.utils import LANGUAGES, prompt_config_to_kwargs


# --- 本地模型函数 ---
@st.cache_resource
def load_helsinki_model(model_name):
    try:
        tokenizer = AutoTokenizer.from_pretrained(model_name)
        model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
        return tokenizer, model
    except Exception as e:
        raise RuntimeError(f"Helsinki Model loading failed: {e}")

def helsinki_translate_paragraphs(paragraphs, language, direction_str, status_container):
    if not direction_str or "en-zh" in direction_str:
        model_name = "Helsinki-NLP/opus-mt-en-zh"
    elif "zh-en" in direction_str:
        model_name = "Helsinki-NLP/opus-mt-zh-en"
    else:
        raise ValueError(f"未知的 Helsinki-NLP 翻译方向: {direction_str}")

    tokenizer, model = load_helsinki_model(model_name)
    translated_paragraphs = []
    pbar_container = status_container.container()
    pbar = pbar_container.progress(0, text="Helsinki模型翻译进度: 0%")
    for i, para in enumerate(paragraphs):
        if para.strip():
            inputs = tokenizer(para, return_tensors="pt", padding=True, truncation=True, max_length=512)
            translated_tokens = model.generate(**inputs)
            translated_text = tokenizer.batch_decode(translated_tokens, skip_special_tokens=True)[0]
            translated_paragraphs.append(translated_text)
        else:
            translated_paragraphs.append("")
        progress = (i + 1) / len(paragraphs)
        if pbar:
            pbar.progress(progress, text=f"Helsinki模型翻译进度: {i+1}/{len(paragraphs)}")
    return translated_paragraphs

# --- 通用工具 ---
@contextlib.contextmanager
def st_redirect(streamlit_element):
    original_stdout = sys.stdout
    output_catcher = StringIO()
    sys.stdout = output_catcher
    try:
        yield
    finally:
        sys.stdout = original_stdout

# --- 【核心】独立的任务执行器 ---
def run_translation_task(task_id, input_file, engine_id, language, api_key, kwargs_options):
    DB_FILE = Path("tasks_db.json")
    def update_task_status(status, result_file=None, error_message=None):
        tasks = {}
        if DB_FILE.exists():
            lock_file = DB_FILE.with_suffix(".lock")
            while lock_file.exists(): time.sleep(0.1)
            try:
                lock_file.touch()
                with open(DB_FILE, "r", encoding="utf-8") as f: tasks = json.load(f)
            finally:
                if lock_file.exists(): lock_file.unlink()
        if task_id in tasks:
            tasks[task_id]["status"] = status
            tasks[task_id]["updated_at"] = datetime.now().isoformat()
            if result_file: tasks[task_id]["result_file"] = str(result_file)
            if error_message: tasks[task_id]["error"] = error_message
        lock_file = DB_FILE.with_suffix(".lock")
        while lock_file.exists(): time.sleep(0.1)
        try:
            lock_file.touch()
            with open(DB_FILE, "w", encoding="utf-8") as f: json.dump(tasks, f, indent=2, ensure_ascii=False)
        finally:
            if lock_file.exists(): lock_file.unlink()
    try:
        update_task_status("🏃‍♂️ 运行中...")
        class MockStreamlitElement:
            def empty(self): return self
            def container(self): return self
            def progress(self, *args, **kwargs): pass
            def text(self, *args, **kwargs): pass
            def info(self, *args, **kwargs): pass
        mock_status_container = MockStreamlitElement()
        output_file = translate_book_processing(input_file, engine_id, api_key, language, mock_status_container, **kwargs_options)
        update_task_status("✅ 已完成", result_file=output_file)
    except Exception as e:
        error_details = traceback.format_exc()
        update_task_status("❌ 失败", error_message=f"{str(e)}\n\nTraceback:\n{error_details}")

# --- 【核心】原始翻译处理函数 ---
def translate_book_processing(input_file_path, engine_id, api_key, language, status_container, **kwargs):
    book_type = input_file_path.split('.')[-1]
    
    # 【修复】从带有时间戳的输入文件名中,恢复出原始文件名
    # 例如:从 "100849_【初祥】发烧.txt" 变为 "【初祥】发烧.txt"
    original_name_with_ext = "_".join(Path(input_file_path).name.split('_')[1:])
    name_only, ext_only = os.path.splitext(original_name_with_ext)
    
    output_dir = Path("outputs")
    output_dir.mkdir(exist_ok=True)
    # 【修复】使用恢复后的原始文件名来创建双语文件名
    output_file_path = str(output_dir / f"{name_only}_bilingual{ext_only}")

    if engine_id == "local_helsinki":
        single_translate = kwargs.get("single_translate", False)
        book_loader_class = BOOK_LOADER_DICT.get(book_type)
        if not book_loader_class: raise ValueError(f"不支持的文件格式: {book_type}")
        temp_loader_options = {"model": lambda k, l, **kw: None, "key": "", "language": language, "resume": kwargs.get("resume", False)}
        temp_loader_options[f"{book_type}_name"] = input_file_path
        book = book_loader_class(**temp_loader_options)
        if book_type == "epub":
            new_book = book._make_new_book(book.origin_book)
            all_items = list(book.origin_book.get_items_of_type(ebooklib.ITEM_DOCUMENT))
            for item in book.origin_book.get_items():
                if item.get_type() != ebooklib.ITEM_DOCUMENT: new_book.add_item(item)
            for item in all_items:
                soup = book.bs(item.content, "html.parser")
                tags_to_translate = kwargs.get("translate_tags", "p").split(",")
                p_list = soup.findAll(tags_to_translate)
                original_texts = [p.get_text() for p in p_list]
                translated_texts = helsinki_translate_paragraphs(original_texts, language, kwargs.get("direction"), status_container)
                book.helper = book_loader_class.helper_class(translate_model=None)
                for p, trans in zip(p_list, translated_texts):
                    book.helper.insert_trans(p, trans, "", single_translate)
                item.content = soup.encode()
                new_book.add_item(item)
            ebooklib.epub.write_epub(output_file_path, new_book, {})
        else:
            with open(input_file_path, 'r', encoding='utf-8') as f: paragraphs = f.read().split('\n\n')
            translated_paragraphs = helsinki_translate_paragraphs(paragraphs, language, kwargs.get("direction"), status_container)
            with open(output_file_path, 'w', encoding='utf-8') as f:
                for orig, trans in zip(paragraphs, translated_paragraphs):
                    if not single_translate: f.write(orig + '\n\n')
                    f.write(trans + '\n\n')
        return output_file_path

    translate_model_class = MODEL_DICT.get(engine_id)
    if not translate_model_class: raise ValueError(f"不支持的翻译引擎: {engine_id}")
    loader_arg_name = f"{book_type}_name"
    language_value = LANGUAGES.get(language, language)
    loader_options = {
        loader_arg_name: input_file_path, "model": translate_model_class,
        "key": api_key or "None", "language": language_value, "resume": kwargs.get("resume", False),
        "is_test": kwargs.get("is_test", False), "test_num": kwargs.get("test_num", 10),
        "prompt_config": kwargs.get("prompt_config"), "single_translate": kwargs.get("single_translate", False),
        "context_flag": kwargs.get("context_flag", False), "temperature": kwargs.get("temperature", 1.0),
        "source_lang": kwargs.get("source_lang", "auto"), "model_api_base": kwargs.get("api_base"),
    }
    proxy = kwargs.get('proxy')
    if proxy: os.environ["http_proxy"] = os.environ["https_proxy"] = proxy
    book_translator = BOOK_LOADER_DICT.get(book_type)(**loader_options)
    with st_redirect(status_container):
        book_translator.make_bilingual_book()

    if os.path.exists(output_file_path):
        return output_file_path
    
    # Fallback check for original library's naming convention
    # e.g., for input 'uploads/123_test.txt', it might create 'uploads/123_test_bilingual.txt'
    temp_output_path = f"{os.path.splitext(input_file_path)[0]}_bilingual{ext_only}"
    if os.path.exists(temp_output_path):
        os.rename(temp_output_path, output_file_path)
        return output_file_path
        
    raise FileNotFoundError(f"翻译完成,但未在预期的输出路径找到文件: {output_file_path}")