[
  {
    "path": ".dockerignore",
    "content": "# 忽略 Git 相关文件\n.git\n.gitignore\n\n# 忽略 Python 缓存和编译文件\n__pycache__/\n**/__pycache__/\n*.pyc\n*.pyo\n*.pyd\n\n# 忽略虚拟环境相关文件夹\n.Python\nenv/\nvenv/\n\n# 忽略 pip 日志\npip-log.txt\npip-delete-this-directory.txt\n\n# 忽略测试和覆盖率相关文件\n.tox/\n.coverage\n.coverage.*\n.cache/\nnosetests.xml\ncoverage.xml\n*.cover\n\n# 忽略日志文件\n*.log\nlogs/*.log\n\n# 忽略 pytest 缓存\n.pytest_cache/\n\n# 忽略项目根目录下的 lib 文件夹\nlib/\n\n# 忽略 IDE 配置文件\n.idea/\n.vscode/\n"
  },
  {
    "path": ".gitignore",
    "content": "__pycache__\nvenv\n*.local\n"
  },
  {
    "path": "All_Translation.py",
    "content": "import time\r\nimport os\r\nimport Deepl_Translation as dt\r\nimport YouDao_translation as yt\r\nimport Bing_translation as bt\r\nimport LLMS_translation as lt\r\nimport asyncio\r\nfrom functools import wraps\r\nimport threading\r\nfrom queue import Queue\r\n\r\n# 创建一个信号量，限制并发为1（串行处理）\r\ntranslation_semaphore = asyncio.Semaphore(1)\r\n# 创建一个队列处理锁，确保队列操作线程安全\r\nqueue_lock = threading.Lock()\r\n# 创建翻译请求队列\r\ntranslation_queue = Queue()\r\n# 标记队列处理器是否已启动\r\nqueue_processor_started = False\r\n\r\ndef retry_on_error(max_retries=2, delay=1):\r\n    def decorator(func):\r\n        @wraps(func)\r\n        def wrapper_sync(*args, **kwargs):\r\n            retries = 0\r\n            while retries <= max_retries:\r\n                try:\r\n                    return func(*args, **kwargs)\r\n                except Exception as e:\r\n                    retries += 1\r\n                    if retries <= max_retries:\r\n                        print(f\"Error occurred: {str(e)}\")\r\n                        print(f\"Retrying... (Attempt {retries} of {max_retries})\")\r\n                        time.sleep(delay)\r\n                    else:\r\n                        print(f\"Max retries reached. Skipping... Final error: {str(e)}\")\r\n                        return None\r\n            return None\r\n\r\n        async def wrapper_async(*args, **kwargs):\r\n            retries = 0\r\n            while retries <= max_retries:\r\n                try:\r\n                    return await func(*args, **kwargs)\r\n                except Exception as e:\r\n                    retries += 1\r\n                    if retries <= max_retries:\r\n                        print(f\"Error occurred: {str(e)}\")\r\n                        print(f\"Retrying... (Attempt {retries} of {max_retries})\")\r\n                        await asyncio.sleep(delay)\r\n                    else:\r\n                        print(f\"Max retries reached. Skipping... Final error: {str(e)}\")\r\n                        return None\r\n            return None\r\n\r\n        return wrapper_async if asyncio.iscoroutinefunction(func) else wrapper_sync\r\n    return decorator\r\n\r\n# 队列处理器函数\r\ndef process_translation_queue():\r\n    global queue_processor_started\r\n\r\n    # 在这里只创建一次事件循环\r\n    loop = asyncio.new_event_loop()\r\n    asyncio.set_event_loop(loop)\r\n\r\n    while True:\r\n        task = translation_queue.get()\r\n        if task is None:  # 终止信号\r\n            translation_queue.task_done()\r\n            break\r\n        try:\r\n            func, args, kwargs, result_holder = task\r\n            # 这里直接用上面创建的 loop 执行\r\n            result = loop.run_until_complete(func(*args, **kwargs))\r\n            result_holder['result'] = result\r\n        except Exception as e:\r\n            print(f\"Error processing translation task: {str(e)}\")\r\n            result_holder['result'] = None\r\n        finally:\r\n            translation_queue.task_done()\r\n\r\n    # 跳出循环后，才一次性关闭事件循环\r\n    # 先清理异步生成器\r\n    loop.run_until_complete(loop.shutdown_asyncgens())\r\n    # 然后再 close\r\n    loop.close()\r\n# 启动队列处理线程\r\ndef ensure_queue_processor():\r\n    global queue_processor_started\r\n    with queue_lock:\r\n        if not queue_processor_started:\r\n            threading.Thread(target=process_translation_queue, daemon=True).start()\r\n            queue_processor_started = True\r\n\r\nclass Online_translation:\r\n    def __init__(self, original_language, target_language, translation_type, texts_to_process=[]):\r\n        self.model_name = f\"opus-mt-{original_language}-{target_language}\"\r\n        self.original_text = texts_to_process\r\n        self.target_language = target_language\r\n        self.original_lang = original_language\r\n        self.translation_type = translation_type\r\n        # 确保队列处理器已启动\r\n        ensure_queue_processor()\r\n\r\n    def run_async(self, coro):\r\n        # 创建结果容器\r\n        result_holder = {'result': None}\r\n        \r\n        # 将协程包装为任务并放入队列\r\n        translation_queue.put((self._run_coro_with_semaphore, [coro], {}, result_holder))\r\n        \r\n        # 等待任务完成\r\n        translation_queue.join()\r\n        \r\n        # 返回结果\r\n        return result_holder['result']\r\n    \r\n    async def _run_coro_with_semaphore(self, coro):\r\n        # 使用信号量确保串行执行\r\n        async with translation_semaphore:\r\n            return await coro\r\n\r\n    def translation(self):\r\n        print('翻译api', self.translation_type)\r\n        if self.translation_type == 'AI302':\r\n            translated_list = self.run_async(self.AI302_translation())\r\n        elif self.translation_type == 'deepl':\r\n            translated_list = self.deepl_translation()\r\n        elif self.translation_type == 'youdao':\r\n            translated_list = self.youdao_translation()\r\n        elif self.translation_type == 'bing':\r\n            translated_list = self.bing_translation()\r\n        elif self.translation_type == 'openai':\r\n            translated_list = self.run_async(self.openai_translation())\r\n        elif self.translation_type == 'deepseek':\r\n            translated_list = self.run_async(self.deepseek_translation())\r\n        elif self.translation_type == 'Doubao':\r\n            translated_list = self.run_async(self.Doubao_translation())\r\n        elif self.translation_type == 'Qwen':\r\n            translated_list = self.run_async(self.Qwen_translation())\r\n        elif self.translation_type == 'Grok':\r\n            translated_list = self.run_async(self.Grok_translation())\r\n        elif self.translation_type == 'ThirdParty':\r\n            translated_list = self.run_async(self.ThirdParty_translation())\r\n        elif self.translation_type == 'GLM':\r\n            translated_list = self.run_async(self.GLM_translation())\r\n        else:\r\n            translated_list = self.deepl_translation()\r\n\r\n        return translated_list\r\n\r\n    @retry_on_error()\r\n    def deepl_translation(self):\r\n        translated_texts = dt.translate(\r\n            texts=self.original_text,\r\n            original_lang=self.original_lang,\r\n            target_lang=self.target_language\r\n        )\r\n        return translated_texts\r\n\r\n    @retry_on_error()\r\n    def youdao_translation(self):\r\n        translated_texts = yt.translate(\r\n            texts=self.original_text,\r\n            original_lang=self.original_lang,\r\n            target_lang=self.target_language\r\n        )\r\n        return translated_texts\r\n\r\n    @retry_on_error()\r\n    def bing_translation(self):\r\n        try:\r\n            translated_texts = bt.translate(\r\n                texts=self.original_text,\r\n                original_lang=self.original_lang,\r\n                target_lang=self.target_language\r\n            )\r\n            print(f\"Bing translation completed: {len(translated_texts)} texts processed\")\r\n            return translated_texts\r\n        except Exception as e:\r\n            print(f\"Error in Bing translation: {e}\")\r\n            return [\"\"] * len(self.original_text)\r\n\r\n    @retry_on_error()\r\n    async def AI302_translation(self):\r\n        translator = lt.AI302_translation()\r\n        translated_texts = await translator.translate(\r\n            texts=self.original_text,\r\n            original_lang=self.original_lang,\r\n            target_lang=self.target_language\r\n        )\r\n        return translated_texts\r\n\r\n    @retry_on_error()\r\n    async def openai_translation(self):\r\n        translator = lt.Openai_translation()\r\n        translated_texts = await translator.translate(\r\n            texts=self.original_text,\r\n            original_lang=self.original_lang,\r\n            target_lang=self.target_language\r\n        )\r\n        return translated_texts\r\n\r\n    @retry_on_error()\r\n    async def deepseek_translation(self):\r\n        translator = lt.Deepseek_translation()\r\n        translated_texts = await translator.translate(\r\n            texts=self.original_text,\r\n            original_lang=self.original_lang,\r\n            target_lang=self.target_language\r\n        )\r\n        return translated_texts\r\n\r\n    @retry_on_error()\r\n    async def Doubao_translation(self):\r\n        translator = lt.Doubao_translation()\r\n        translated_texts = await translator.translate(\r\n            texts=self.original_text,\r\n            original_lang=self.original_lang,\r\n            target_lang=self.target_language\r\n        )\r\n        return translated_texts\r\n\r\n    @retry_on_error()\r\n    async def Qwen_translation(self):\r\n        translator = lt.Qwen_translation()\r\n        translated_texts = await translator.translate(\r\n            texts=self.original_text,\r\n            original_lang=self.original_lang,\r\n            target_lang=self.target_language\r\n        )\r\n        return translated_texts\r\n\r\n    @retry_on_error()\r\n    async def Grok_translation(self):\r\n        translator = lt.Grok_translation()\r\n        try:\r\n            translated_texts = await translator.translate(\r\n                texts=self.original_text,\r\n                original_lang=self.original_lang,\r\n                target_lang=self.target_language\r\n            )\r\n            print(f\"Grok translation completed: {len(translated_texts)} texts processed\")\r\n            return translated_texts\r\n        except Exception as e:\r\n            print(f\"Error in Grok translation: {e}\")\r\n            return [\"\"] * len(self.original_text)\r\n\r\n    @retry_on_error()\r\n    async def ThirdParty_translation(self):\r\n        translator = lt.ThirdParty_translation()\r\n        try:\r\n            translated_texts = await translator.translate(\r\n                texts=self.original_text,\r\n                original_lang=self.original_lang,\r\n                target_lang=self.target_language\r\n            )\r\n            print(f\"ThirdParty translation completed: {len(translated_texts)} texts processed\")\r\n            return translated_texts\r\n        except Exception as e:\r\n            print(f\"Error in ThirdParty translation: {e}\")\r\n            return [\"\"] * len(self.original_text)\r\n\r\n    @retry_on_error()\r\n    async def GLM_translation(self):\r\n        translator = lt.GLM_translation()\r\n        try:\r\n            translated_texts = await translator.translate(\r\n                texts=self.original_text,\r\n                original_lang=self.original_lang,\r\n                target_lang=self.target_language\r\n            )\r\n            print(f\"GLM translation completed: {len(translated_texts)} texts processed\")\r\n            return translated_texts\r\n        except Exception as e:\r\n            print(f\"Error in GLM translation: {e}\")\r\n            return [\"\"] * len(self.original_text)\r\n\r\n# 确保程序退出前清理资源\r\nimport atexit\r\n\r\n@atexit.register\r\ndef cleanup():\r\n    # 发送终止信号\r\n    if queue_processor_started:\r\n        translation_queue.put(None)\r\n        # 给队列处理器一些时间来处理终止信号\r\n        translation_queue.join()\r\n\r\nt = time.time()\r\n\r\ndef split_text_to_fit_token_limit(text, encoder, index_text, max_length=280):\r\n    tokens = encoder.encode(text)\r\n    if len(tokens) <= max_length:\r\n        return [(text, len(tokens), index_text)]\r\n\r\n    split_points = [i for i, token in enumerate(tokens) if encoder.decode([token]).strip() in [' ', '.', '?', '!','！','？','。']]\r\n    parts = []\r\n    last_split = 0\r\n    for i, point in enumerate(split_points + [len(tokens)]):\r\n        if point - last_split > max_length:\r\n            part_tokens = tokens[last_split:split_points[i - 1]]\r\n            parts.append((encoder.decode(part_tokens), len(part_tokens), index_text))\r\n            last_split = split_points[i - 1]\r\n        elif i == len(split_points):\r\n            part_tokens = tokens[last_split:]\r\n            parts.append((encoder.decode(part_tokens), len(part_tokens), index_text))\r\n\r\n    return parts\r\n\r\ndef process_texts(texts, encoder):\r\n    processed_texts = []\r\n    for i, text in enumerate(texts):\r\n        sub_texts = split_text_to_fit_token_limit(text, encoder, i)\r\n        processed_texts.extend(sub_texts)\r\n    return processed_texts\r\n\r\ndef calculate_split_points(processed_texts, max_tokens=425):\r\n    split_points = []\r\n    current_tokens = 0\r\n\r\n    for i in range(len(processed_texts) - 1):\r\n        current_tokens = processed_texts[i][1]\r\n        next_tokens = processed_texts[i + 1][1]\r\n\r\n        if current_tokens + next_tokens > max_tokens:\r\n            split_points.append(i)\r\n\r\n    split_points.append(len(processed_texts) - 1)\r\n\r\n    return split_points\r\n#\r\n# def translate(texts,original_language,target_language):\r\n#     from transformers import pipeline, AutoTokenizer\r\n#\r\n#     model_name = f\"./opus-mt-{original_language}-{target_language}\"\r\n#     pipe = pipeline(\"translation\", model=model_name)\r\n#     tokenizer = AutoTokenizer.from_pretrained(model_name)\r\n#\r\n#     result = pipe(texts)\r\n#\r\n#     result_values = [d['translation_text'] for d in result]\r\n#\r\n#     return result_values\r\n#\r\n# def batch_translate(processed_texts, split_points,original_language,target_language):\r\n#     translated_texts = []\r\n#     index_mapping = {}\r\n#\r\n#     start_index = 0\r\n#\r\n#     for split_point in split_points:\r\n#         batch = processed_texts[start_index:split_point + 1]\r\n#         batch_texts = [text for text, _, _ in batch]\r\n#         translated_batch = translate(texts=batch_texts,original_language=original_language,target_language=target_language)\r\n#\r\n#         for translated_text, (_, _, int_value) in zip(translated_batch, batch):\r\n#             if int_value in index_mapping:\r\n#                 translated_texts[index_mapping[int_value]] += \" \" + translated_text\r\n#             else:\r\n#                 index_mapping[int_value] = len(translated_texts)\r\n#                 translated_texts.append(translated_text)\r\n#\r\n#         start_index = split_point + 1\r\n#\r\n#     return translated_texts\r\n#\r\n"
  },
  {
    "path": "Bing_translation.py",
    "content": "import re\r\nimport requests\r\nimport time\r\nimport threading\r\nimport asyncio\r\nimport aiohttp\r\nfrom concurrent.futures import ThreadPoolExecutor\r\n\r\ndef translate(texts, original_lang, target_lang):\r\n    \"\"\"\r\n    使用Bing翻译API翻译文本列表 - 高性能实现\r\n    \r\n    Args:\r\n        texts: 要翻译的文本列表\r\n        original_lang: 源语言代码\r\n        target_lang: 目标语言代码\r\n        \r\n    Returns:\r\n        翻译后的文本列表\r\n    \"\"\"\r\n    # 确保输入文本为列表格式\r\n    if isinstance(texts, str):\r\n        texts = [texts]\r\n    \r\n    # 如果文本量小，使用简单的并发线程池\r\n    if len(texts) <= 20:\r\n        return translate_with_threadpool(texts, original_lang, target_lang)\r\n    \r\n    # 对于大量文本，使用异步IO处理\r\n    return translate_with_asyncio(texts, original_lang, target_lang)\r\n\r\n\r\ndef translate_with_threadpool(texts, original_lang, target_lang, max_workers=5):\r\n    \"\"\"使用线程池并发翻译小批量文本\"\"\"\r\n    translator = BingTranslator(lang_in=original_lang, lang_out=target_lang)\r\n    translated_texts = [\"\"] * len(texts)\r\n    \r\n    def translate_one(index, text):\r\n        try:\r\n            translated_texts[index] = translator.do_translate(text)\r\n        except Exception as e:\r\n            print(f\"翻译文本时出错 (索引 {index}): {e}\")\r\n            translated_texts[index] = \"\"\r\n    \r\n    # 使用线程池并发处理\r\n    with ThreadPoolExecutor(max_workers=max_workers) as executor:\r\n        futures = [executor.submit(translate_one, i, text) \r\n                  for i, text in enumerate(texts)]\r\n        \r\n        # 等待所有任务完成\r\n        for future in futures:\r\n            future.result()\r\n    \r\n    return translated_texts\r\n\r\n\r\ndef translate_with_asyncio(texts, original_lang, target_lang):\r\n    \"\"\"使用asyncio异步处理大批量文本\"\"\"\r\n    # 定义异步主函数\r\n    async def main():\r\n        translator = AsyncBingTranslator(lang_in=original_lang, lang_out=target_lang)\r\n        return await translator.translate_batch(texts)\r\n    \r\n    # 如果当前线程没有事件循环，创建一个新的\r\n    try:\r\n        loop = asyncio.get_event_loop()\r\n    except RuntimeError:\r\n        loop = asyncio.new_event_loop()\r\n        asyncio.set_event_loop(loop)\r\n    \r\n    # 运行异步函数并返回结果\r\n    return loop.run_until_complete(main())\r\n\r\n\r\ndef split_text_intelligently(text, max_length=1000):\r\n    \"\"\"智能分段文本，尽量在句子边界处断开\"\"\"\r\n    if len(text) <= max_length:\r\n        return [text]\r\n        \r\n    parts = []\r\n    start = 0\r\n    \r\n    while start < len(text):\r\n        # 如果剩余文本不足max_length，直接添加\r\n        if len(text) - start <= max_length:\r\n            parts.append(text[start:])\r\n            break\r\n            \r\n        # 计算当前段落的结束位置\r\n        end = start + max_length\r\n        \r\n        # 尝试在句子结束处断开（优先级：段落 > 句号 > 逗号 > 空格）\r\n        paragraph_break = text.rfind('\\n', start, end)\r\n        if paragraph_break != -1 and paragraph_break > start + max_length * 0.5:\r\n            end = paragraph_break + 1\r\n        else:\r\n            # 寻找句号、问号、感叹号等\r\n            for sep in ['. ', '。', '？', '！', '? ', '! ']:\r\n                pos = text.rfind(sep, start, end)\r\n                if pos != -1 and pos > start + max_length * 0.5:\r\n                    end = pos + len(sep)\r\n                    break\r\n            else:\r\n                # 如果没找到句号，尝试在逗号处断开\r\n                for sep in [', ', '，', '; ', '；']:\r\n                    pos = text.rfind(sep, start, end)\r\n                    if pos != -1 and pos > start + max_length * 0.7:\r\n                        end = pos + len(sep)\r\n                        break\r\n                else:\r\n                    # 实在没有好的断点就在空格处断开\r\n                    pos = text.rfind(' ', start + max_length * 0.8, end)\r\n                    if pos != -1:\r\n                        end = pos + 1\r\n        \r\n        parts.append(text[start:end])\r\n        start = end\r\n    \r\n    return parts\r\n\r\n\r\nclass BingTranslator:\r\n    name = \"bing\"\r\n    lang_map = {\"zh\": \"zh-Hans\"}\r\n    \r\n    # 会话参数缓存\r\n    _cache_lock = threading.Lock()\r\n    _sid_cache = None\r\n    _sid_timestamp = 0\r\n    _sid_cache_ttl = 300  # 5分钟缓存有效期\r\n\r\n    def __init__(self, lang_in, lang_out, model=None, ignore_cache=False):\r\n        # 处理语言代码映射\r\n        self.lang_in = self.lang_map.get(lang_in, lang_in)\r\n        self.lang_out = self.lang_map.get(lang_out, lang_out)\r\n        \r\n        # 自动语言检测处理\r\n        if self.lang_in == \"auto\":\r\n            self.lang_in = \"auto-detect\"\r\n            \r\n        self.model = model\r\n        self.ignore_cache = ignore_cache\r\n        self.session = requests.Session()\r\n        self.endpoint = \"https://www.bing.com/translator\"\r\n        self.headers = {\r\n            \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0\",\r\n        }\r\n\r\n    def find_sid(self):\r\n        \"\"\"获取必要的会话参数，使用缓存减少请求\"\"\"\r\n        current_time = time.time()\r\n        \r\n        # 检查缓存是否有效\r\n        with self._cache_lock:\r\n            if (not self.ignore_cache and \r\n                BingTranslator._sid_cache is not None and \r\n                (current_time - BingTranslator._sid_timestamp) < BingTranslator._sid_cache_ttl):\r\n                return BingTranslator._sid_cache\r\n        \r\n        # 缓存无效，重新获取参数\r\n        response = self.session.get(self.endpoint, headers=self.headers)\r\n        response.raise_for_status()\r\n        url = response.url[:-10]\r\n        ig = re.findall(r\"\\\"ig\\\":\\\"(.*?)\\\"\", response.text)[0]\r\n        iid = re.findall(r\"data-iid=\\\"(.*?)\\\"\", response.text)[-1]\r\n        key, token = re.findall(\r\n            r\"params_AbusePreventionHelper\\s=\\s\\[(.*?),\\\"(.*?)\\\",\", response.text\r\n        )[0]\r\n        \r\n        # 更新缓存\r\n        result = (url, ig, iid, key, token)\r\n        with self._cache_lock:\r\n            BingTranslator._sid_cache = result\r\n            BingTranslator._sid_timestamp = current_time\r\n        \r\n        return result\r\n\r\n    def do_translate(self, text):\r\n        \"\"\"执行翻译\"\"\"\r\n        if not text or not text.strip():\r\n            return \"\"\r\n            \r\n        # 如果文本超过1000字符，分段翻译\r\n        if len(text) > 1000:\r\n            parts = split_text_intelligently(text)\r\n            translated_parts = []\r\n            \r\n            for part in parts:\r\n                url, ig, iid, key, token = self.find_sid()\r\n                response = self.session.post(\r\n                    f\"{url}ttranslatev3?IG={ig}&IID={iid}\",\r\n                    data={\r\n                        \"fromLang\": self.lang_in,\r\n                        \"to\": self.lang_out,\r\n                        \"text\": part[:1000],  # 确保不超过1000\r\n                        \"token\": token,\r\n                        \"key\": key,\r\n                    },\r\n                    headers=self.headers,\r\n                )\r\n                response.raise_for_status()\r\n                translated_parts.append(response.json()[0][\"translations\"][0][\"text\"])\r\n                \r\n            return ''.join(translated_parts)\r\n        \r\n        url, ig, iid, key, token = self.find_sid()\r\n        response = self.session.post(\r\n            f\"{url}ttranslatev3?IG={ig}&IID={iid}\",\r\n            data={\r\n                \"fromLang\": self.lang_in,\r\n                \"to\": self.lang_out,\r\n                \"text\": text,\r\n                \"token\": token,\r\n                \"key\": key,\r\n            },\r\n            headers=self.headers,\r\n        )\r\n        response.raise_for_status()\r\n        return response.json()[0][\"translations\"][0][\"text\"]\r\n\r\n\r\nclass AsyncBingTranslator:\r\n    \"\"\"异步Bing翻译器实现\"\"\"\r\n    lang_map = {\"zh\": \"zh-Hans\"}\r\n    \r\n    # 会话参数缓存\r\n    _sid_cache = None\r\n    _sid_timestamp = 0\r\n    _sid_cache_ttl = 300  # 5分钟缓存有效期\r\n\r\n    def __init__(self, lang_in, lang_out):\r\n        self.lang_in = self.lang_map.get(lang_in, lang_in)\r\n        self.lang_out = self.lang_map.get(lang_out, lang_out)\r\n        \r\n        if self.lang_in == \"auto\":\r\n            self.lang_in = \"auto-detect\"\r\n            \r\n        self.headers = {\r\n            \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0\",\r\n        }\r\n        self.endpoint = \"https://www.bing.com/translator\"\r\n\r\n    async def find_sid(self, session):\r\n        \"\"\"异步获取会话参数，带缓存\"\"\"\r\n        current_time = time.time()\r\n        \r\n        # 检查缓存是否有效\r\n        if (AsyncBingTranslator._sid_cache is not None and \r\n            (current_time - AsyncBingTranslator._sid_timestamp) < AsyncBingTranslator._sid_cache_ttl):\r\n            return AsyncBingTranslator._sid_cache\r\n        \r\n        # 缓存无效，异步获取新参数\r\n        async with session.get(self.endpoint, headers=self.headers) as response:\r\n            if response.status != 200:\r\n                raise Exception(f\"获取会话参数失败: HTTP {response.status}\")\r\n                \r\n            text = await response.text()\r\n            url = str(response.url)[:-10]\r\n            ig = re.findall(r\"\\\"ig\\\":\\\"(.*?)\\\"\", text)[0]\r\n            iid = re.findall(r\"data-iid=\\\"(.*?)\\\"\", text)[-1]\r\n            key, token = re.findall(\r\n                r\"params_AbusePreventionHelper\\s=\\s\\[(.*?),\\\"(.*?)\\\",\", text\r\n            )[0]\r\n            \r\n            # 更新缓存\r\n            result = (url, ig, iid, key, token)\r\n            AsyncBingTranslator._sid_cache = result\r\n            AsyncBingTranslator._sid_timestamp = current_time\r\n            \r\n            return result\r\n\r\n    async def translate_text(self, session, text):\r\n        \"\"\"翻译单个文本\"\"\"\r\n        if not text or not text.strip():\r\n            return \"\"\r\n        \r\n        # 如果文本超过1000字符，分段翻译\r\n        if len(text) > 1000:\r\n            parts = split_text_intelligently(text)\r\n            translated_parts = []\r\n            \r\n            # 非递归异步处理每个文本块\r\n            for part in parts:\r\n                url, ig, iid, key, token = await self.find_sid(session)\r\n                \r\n                async with session.post(\r\n                    f\"{url}ttranslatev3?IG={ig}&IID={iid}\",\r\n                    data={\r\n                        \"fromLang\": self.lang_in,\r\n                        \"to\": self.lang_out,\r\n                        \"text\": part[:1000],  # 确保不超过1000\r\n                        \"token\": token,\r\n                        \"key\": key,\r\n                    },\r\n                    headers=self.headers,\r\n                ) as response:\r\n                    if response.status == 200:\r\n                        result = await response.json()\r\n                        translated_parts.append(result[0][\"translations\"][0][\"text\"])\r\n                    else:\r\n                        print(f\"翻译请求失败: HTTP {response.status}\")\r\n                        translated_parts.append(\"\")\r\n            \r\n            return ''.join(translated_parts)\r\n        \r\n        try:\r\n            url, ig, iid, key, token = await self.find_sid(session)\r\n            response = await session.post(\r\n                f\"{url}ttranslatev3?IG={ig}&IID={iid}\",\r\n                data={\r\n                    \"fromLang\": self.lang_in,\r\n                    \"to\": self.lang_out,\r\n                    \"text\": text,\r\n                    \"token\": token,\r\n                    \"key\": key,\r\n                },\r\n                headers=self.headers,\r\n            )\r\n            if response.status == 200:\r\n                result = await response.json()\r\n                return result[0][\"translations\"][0][\"text\"]\r\n            else:\r\n                print(f\"翻译请求失败: HTTP {response.status}\")\r\n                return \"\"\r\n        except Exception as e:\r\n            print(f\"翻译过程中发生错误: {e}\")\r\n            print(f\"原文: {text}\")\r\n            return \"\"\r\n\r\n    async def translate_batch(self, texts, batch_size=10, max_concurrent=5):\r\n        \"\"\"批量翻译文本，控制并发数量和请求批次\"\"\"\r\n        async with aiohttp.ClientSession() as session:\r\n            results = [\"\"] * len(texts)\r\n            semaphore = asyncio.Semaphore(max_concurrent)\r\n            \r\n            async def translate_with_limit(index, text):\r\n                retry_count = 0\r\n                max_retries = 10\r\n                backoff_time = 1.0  # 初始重试等待时间\r\n                \r\n                while retry_count < max_retries:\r\n                    try:\r\n                        async with semaphore:\r\n                            # 每批次间隔较小的延迟\r\n                            if index > 0 and index % batch_size == 0:\r\n                                await asyncio.sleep(0.1)\r\n                            \r\n\r\n                            translated = await self.translate_text(session, text)\r\n                            if translated:  # 如果翻译成功\r\n                                results[index] = translated\r\n                                if retry_count > 0:  # 如果是重试成功的\r\n                                    print(f\"第{index}个文本重试成功！\")\r\n                                return\r\n                    except Exception as e:\r\n                        print(f\"第{index}个文本翻译失败 (尝试 {retry_count+1}/{max_retries}): {e}\")\r\n                        print(f\"原文: {text}\")\r\n                    \r\n                    # 如果到这里，说明需要重试\r\n                    retry_count += 1\r\n                    if retry_count < max_retries:\r\n                        print(f\"将在{backoff_time}秒后重试...\")\r\n                        await asyncio.sleep(backoff_time)\r\n                        backoff_time *= 2  # 指数退避策略\r\n                    else:\r\n                        print(f\"已达到最大重试次数，翻译失败\")\r\n                        results[index] = \"\"\r\n            \r\n            # 创建所有任务\r\n            tasks = [\r\n                asyncio.create_task(translate_with_limit(i, text))\r\n                for i, text in enumerate(texts)\r\n            ]\r\n            \r\n            # 等待所有任务完成\r\n            await asyncio.gather(*tasks)\r\n            return results\r\n\r\n\r\n# 测试代码\r\nif __name__ == \"__main__\":\r\n    test_texts = [\"Hello, world!\", \"How are you today?\", \"Python is amazing\", \"I love programming\"]\r\n    results = translate(test_texts, \"en\", \"zh\")\r\n    \r\n    for original, translated in zip(test_texts, results):\r\n        print(f\"Original: {original}\")\r\n        print(f\"Translated: {translated}\")\r\n        print(\"-\" * 30)"
  },
  {
    "path": "Deepl_Translation.py",
    "content": "import deepl\nimport load_config\ndef translate(texts,original_lang,target_lang):\n\n# 你的 DeepL 授权密钥\n\n\n    # 获取指定服务的认证信息\n\n\n    config = load_config.load_config()\n\n    auth_key = config['translation_services']['deepl']['auth_key']\n    # print(auth_key)\n\n    translator = deepl.Translator(auth_key)\n\n    # 要翻译的文本列表\n\n\n    # 翻译文本列表，目标语言设置为中文\n    print(original_lang,target_lang)\n    if original_lang == 'auto':\n        results = translator.translate_text(texts, target_lang=target_lang)\n    else:\n        results = translator.translate_text(texts, source_lang=original_lang, target_lang=target_lang)\n\n\n    # 初始化一个空列表来收集翻译结果\n    translated_texts = []\n\n    # 遍历翻译结果，将它们添加到列表中\n    for result in results:\n        translated_texts.append(result.text)\n    return translated_texts\n\n\n\n"
  },
  {
    "path": "Dockerfile",
    "content": "\n# 1. 使用官方 Python 3.9 的精简版镜像作为基础\nFROM python:3.9-slim\n\n# 2. 如果你需要一些系统库支持，可在此处安装\n#    比如安装 gcc、libssl-dev 等 (仅举例)\n# RUN apt-get update && apt-get install -y --no-install-recommends \\\n#     gcc \\\n#     libssl-dev \\\n#     && rm -rf /var/lib/apt/lists/*\n\n# 3. 设置工作目录\nWORKDIR /app\n\n# 4. 将 requirements.txt 复制到容器内\nCOPY requirements.txt /app/\n\n# 5. 安装 Python 依赖包\nRUN pip install --no-cache-dir -r requirements.txt\n\n# 6. 复制项目源代码到容器内\nCOPY . /app\n\n# 7. 暴露端口 12226（如你的项目需要此端口）\nEXPOSE 12226\n\n# 8. 容器启动时，默认执行 Python 脚本\nCMD [\"python\", \"app.py\"]\n"
  },
  {
    "path": "EbookTranslator/EbookTranslator/All_Translation.py",
    "content": "import time\r\nimport os\r\nfrom .import Deepl_Translation as dt\r\nfrom .import YouDao_translation as yt\r\nfrom .import LLMS_translation as lt\r\nimport asyncio\r\n\r\nloop = asyncio.new_event_loop()\r\nasyncio.set_event_loop(loop)\r\n# #\r\n# Get the encoder of a specific model, assume gpt3.5, tiktoken is extremely fast,\r\n# and the error of this statistical token method is small and can be ignored\r\n\r\n\r\nclass Online_translation:\r\n    def __init__(self, original_language, target_language, translation_type, texts_to_process=[]):\r\n        self.model_name = f\"opus-mt-{original_language}-{target_language}\"\r\n        self.original_text = texts_to_process\r\n        self.target_language = target_language\r\n        self.original_lang = original_language\r\n        self.translation_type = translation_type\r\n\r\n    def run_async(self, coro):\r\n        # 往往只要 run_until_complete()，不手动 close() 即可\r\n        return loop.run_until_complete(coro)\r\n\r\n    def translation(self):\r\n        print('translation api',self.translation_type)\r\n        if self.translation_type == 'deepl':\r\n            translated_list = self.deepl_translation()\r\n        elif self.translation_type == 'youdao':\r\n            translated_list = self.youdao_translation()\r\n        elif self.translation_type == 'bing':\r\n            # 使用同步包装器运行异步函数\r\n            translated_list = self.run_async(self.bing_translation())\r\n        elif self.translation_type == 'openai':\r\n            # 使用同步包装器运行异步函数\r\n            translated_list = self.run_async(self.openai_translation())\r\n        elif self.translation_type == 'deepseek':\r\n            # 使用同步包装器运行异步函数\r\n            translated_list = self.run_async(self.deepseek_translation())\r\n        elif self.translation_type == 'Doubao':\r\n            # 使用同步包装器运行异步函数\r\n            translated_list = self.run_async(self.Doubao_translation())\r\n        elif self.translation_type == 'Qwen':\r\n            # 使用同步包装器运行异步函数\r\n            translated_list = self.run_async(self.Qwen_translation())\r\n        elif self.translation_type == 'Grok':\r\n            # 使用同步包装器运行异步函数\r\n            translated_list = self.run_async(self.Grok_translation())\r\n        elif self.translation_type == 'ThirdParty':\r\n            # 使用同步包装器运行异步函数\r\n            translated_list = self.run_async(self.ThirdParty_translation())\r\n        elif self.translation_type == 'GLM':\r\n            # 使用同步包装器运行异步函数\r\n            translated_list = self.run_async(self.GLM_translation())\r\n        else:\r\n            translated_list = self.deepl_translation()\r\n\r\n        return translated_list\r\n\r\n    def deepl_translation(self):\r\n\r\n        translated_texts = dt.translate(texts=self.original_text,original_lang=self.original_lang,target_lang=self.target_language)\r\n\r\n        return translated_texts\r\n\r\n\r\n    def youdao_translation(self):\r\n\r\n        translated_texts = yt.translate(texts=self.original_text,original_lang=self.original_lang,target_lang=self.target_language)\r\n\r\n        return translated_texts\r\n\r\n\r\n\r\n    async def openai_translation(self):\r\n        translator = lt.Openai_translation()\r\n        translated_texts = await translator.translate(\r\n            texts=self.original_text,\r\n            original_lang=self.original_lang,\r\n            target_lang=self.target_language\r\n        )\r\n        return translated_texts\r\n\r\n    async def deepseek_translation(self):\r\n        translator = lt.Deepseek_translation()\r\n        translated_texts = await translator.translate(\r\n            texts=self.original_text,\r\n            original_lang=self.original_lang,\r\n            target_lang=self.target_language\r\n        )\r\n        return translated_texts\r\n    async def Doubao_translation(self):\r\n        translator = lt.Doubao_translation()\r\n        translated_texts = await translator.translate(\r\n            texts=self.original_text,\r\n            original_lang=self.original_lang,\r\n            target_lang=self.target_language\r\n        )\r\n        return translated_texts\r\n    async def Qwen_translation(self):\r\n        translator = lt.Qwen_translation()\r\n        translated_texts = await translator.translate(\r\n            texts=self.original_text,\r\n            original_lang=self.original_lang,\r\n            target_lang=self.target_language\r\n        )\r\n        return translated_texts\r\n    async def Grok_translation(self):\r\n        translator = lt.Grok_translation()\r\n        try:\r\n            translated_texts = await translator.translate(\r\n                texts=self.original_text,\r\n                original_lang=self.original_lang,\r\n                target_lang=self.target_language\r\n            )\r\n            print(f\"Grok translation completed: {len(translated_texts)} texts processed\")\r\n            return translated_texts\r\n        except Exception as e:\r\n            print(f\"Error in Grok translation: {e}\")\r\n            return [\"\"] * len(self.original_text)\r\n\r\n    async def ThirdParty_translation(self):\r\n        translator = lt.ThirdParty_translation()\r\n        try:\r\n            translated_texts = await translator.translate(\r\n                texts=self.original_text,\r\n                original_lang=self.original_lang,\r\n                target_lang=self.target_language\r\n            )\r\n            print(f\"ThirdParty translation completed: {len(translated_texts)} texts processed\")\r\n            return translated_texts\r\n        except Exception as e:\r\n            print(f\"Error in ThirdParty translation: {e}\")\r\n            return [\"\"] * len(self.original_text)\r\n\r\n    async def GLM_translation(self):\r\n        translator = lt.GLM_translation()\r\n        try:\r\n            translated_texts = await translator.translate(\r\n                texts=self.original_text,\r\n                original_lang=self.original_lang,\r\n                target_lang=self.target_language\r\n            )\r\n            print(f\"GLM translation completed: {len(translated_texts)} texts processed\")\r\n            return translated_texts\r\n        except Exception as e:\r\n            print(f\"Error in GLM translation: {e}\")\r\n            return [\"\"] * len(self.original_text)\r\n\r\n    async def bing_translation(self):\r\n        translator = lt.Bing_translation()\r\n        try:\r\n            translated_texts = await translator.translate(\r\n                texts=self.original_text,\r\n                original_lang=self.original_lang,\r\n                target_lang=self.target_language\r\n            )\r\n            print(f\"Bing translation completed: {len(translated_texts)} texts processed\")\r\n            return translated_texts\r\n        except Exception as e:\r\n            print(f\"Error in Bing translation: {e}\")\r\n            return [\"\"] * len(self.original_text)\r\n\r\n\r\nt = time.time()\r\ndef split_text_to_fit_token_limit(text, encoder, index_text, max_length=280):\r\n    tokens = encoder.encode(text)\r\n    if len(tokens) <= max_length:\r\n        return [(text, len(tokens), index_text)]  # Return text along with its token count and original index 返回文本及其标记计数和原始索引\r\n\r\n    # Pre-calculate possible split points (spaces, periods, etc.)\r\n    split_points = [i for i, token in enumerate(tokens) if encoder.decode([token]).strip() in [' ', '.', '?', '!','！','？','。']]\r\n    parts = []\r\n    last_split = 0\r\n    for i, point in enumerate(split_points + [len(tokens)]):  # Ensure the last segment is included\r\n        if point - last_split > max_length:\r\n            part_tokens = tokens[last_split:split_points[i - 1]]\r\n            parts.append((encoder.decode(part_tokens), len(part_tokens), index_text))\r\n            last_split = split_points[i - 1]\r\n        elif i == len(split_points):  # Handle the last part\r\n            part_tokens = tokens[last_split:]\r\n            parts.append((encoder.decode(part_tokens), len(part_tokens), index_text))\r\n\r\n    return parts\r\n\r\ndef process_texts(texts, encoder):\r\n    processed_texts = []\r\n    for i, text in enumerate(texts):\r\n        sub_texts = split_text_to_fit_token_limit(text, encoder, i)\r\n        processed_texts.extend(sub_texts)\r\n    return processed_texts\r\n\r\n\r\n\r\ndef calculate_split_points(processed_texts, max_tokens=425):\r\n    split_points = []  # 存储划分点的索引\r\n    current_tokens = 0  # 当前累积的token数\r\n\r\n    for i in range(len(processed_texts) - 1):  # 遍历到倒数第二个元素\r\n        current_tokens = processed_texts[i][1]\r\n        next_tokens = processed_texts[i + 1][1]\r\n\r\n        # 如果当前元素和下一个元素的token数之和超过了限制\r\n        if current_tokens + next_tokens > max_tokens:\r\n            split_points.append(i)  # 当前元素作为一个划分点\r\n        # 注意：这里不需要重置 current_tokens，因为每次循环都是新的一对元素\r\n\r\n    # 最后一个元素总是一个划分点，因为它后面没有元素与之相邻\r\n    split_points.append(len(processed_texts) - 1)\r\n\r\n    return split_points\r\n\r\n\r\ndef translate(texts,original_language,target_language):\r\n    # 这里仅返回相同的文本列表作为示例，实际中应返回翻译后的文本\r\n    from transformers import pipeline, AutoTokenizer\r\n\r\n    model_name = f\"./opus-mt-{original_language}-{target_language}\"  # 请替换为实际路径\r\n    # 创建翻译管道，指定本地模型路径\r\n    pipe = pipeline(\"translation\", model=model_name)\r\n    # 获取tokenizer，指定本地模型路径\r\n    tokenizer = AutoTokenizer.from_pretrained(model_name)\r\n\r\n    result = pipe(texts)\r\n\r\n\r\n    # 提取值并组合成新的列表\r\n    result_values = [d['translation_text'] for d in result]\r\n\r\n    return result_values\r\n\r\n\r\n\r\ndef batch_translate(processed_texts, split_points,original_language,target_language):\r\n    translated_texts = []  # 存储翻译后的文本的列表\r\n    index_mapping = {}  # 存储每个int_value对应在translated_texts中的索引\r\n\r\n    start_index = 0  # 当前批次的起始索引\r\n\r\n    # 遍历划分点，按批次翻译文本\r\n    for split_point in split_points:\r\n        # 提取当前批次的文本（不包括划分点的下一个元素）\r\n        batch = processed_texts[start_index:split_point + 1]\r\n        batch_texts = [text for text, _, _ in batch]\r\n        # 翻译函数\r\n        translated_batch = translate(texts=batch_texts,original_language=original_language,target_language=target_language)\r\n\r\n        # 遍历当前批次的翻译结果\r\n        for translated_text, (_, _, int_value) in zip(translated_batch, batch):\r\n            if int_value in index_mapping:\r\n                # 如果键已存在，将新的翻译文本与原有的值拼接\r\n                translated_texts[index_mapping[int_value]] += \" \" + translated_text\r\n            else:\r\n                # 如果键不存在，直接添加到列表，并记录其索引\r\n                index_mapping[int_value] = len(translated_texts)\r\n                translated_texts.append(translated_text)\r\n\r\n        # 更新下一批次的起始索引\r\n        start_index = split_point + 1\r\n\r\n    return translated_texts\r\n\r\n"
  },
  {
    "path": "EbookTranslator/EbookTranslator/Deepl_Translation.py",
    "content": "import deepl\nfrom .import load_config\ndef translate(texts,original_lang,target_lang):\n\n# 你的 DeepL 授权密钥\n\n\n    # 获取指定服务的认证信息\n\n\n    config = load_config.load_config()\n\n    auth_key = config['translation_services']['deepl']['auth_key']\n    # print(auth_key)\n\n    translator = deepl.Translator(auth_key)\n\n    # 要翻译的文本列表\n\n\n    # 翻译文本列表，目标语言设置为中文\n    print(original_lang,target_lang)\n    if original_lang == 'auto':\n        results = translator.translate_text(texts, target_lang=target_lang)\n    else:\n        results = translator.translate_text(texts, source_lang=original_lang, target_lang=target_lang)\n\n\n    # 初始化一个空列表来收集翻译结果\n    translated_texts = []\n\n    # 遍历翻译结果，将它们添加到列表中\n    for result in results:\n        translated_texts.append(result.text)\n    return translated_texts\n\n\n\n"
  },
  {
    "path": "EbookTranslator/EbookTranslator/LLMS_translation.py",
    "content": "import asyncio\r\nimport aiohttp\r\nimport re\r\nimport requests\r\n\r\nfrom . import load_config\r\n\r\nclass Openai_translation:\r\n    def __init__(self):\r\n        config = load_config.load_config()\r\n        self.api_key = config['translation_services']['openai']['auth_key']\r\n        self.url = \"https://api.openai.com/v1/chat/completions\"\r\n        self.headers = {\r\n            \"Authorization\": f\"Bearer {self.api_key}\",\r\n            \"Content-Type\": \"application/json\"\r\n        }\r\n        self.model = config['translation_services']['openai']['model_name']\r\n\r\n    async def translate_single(self, session, text, original_lang, target_lang):\r\n        \"\"\"单个文本的异步翻译\"\"\"\r\n        payload = {\r\n            \"model\": self.model,\r\n            \"messages\": [\r\n                {\r\n                    \"role\": \"system\",\r\n                    \"content\": f\"You are a professional translator. Translate from {original_lang} to {target_lang}.Return only the translations\"\r\n                },\r\n                {\r\n                    \"role\": \"user\",\r\n                    \"content\": text\r\n                }\r\n            ],\r\n            \"response_format\": {\r\n                \"type\": \"text\"\r\n            },\r\n            \"temperature\": 0.3,\r\n            \"top_p\": 0.9\r\n        }\r\n\r\n        try:\r\n            async with session.post(self.url, headers=self.headers, json=payload) as response:\r\n                if response.status == 200:\r\n                    result = await response.json()\r\n                    return result['choices'][0]['message']['content'].strip()\r\n                else:\r\n                    print(f\"Error: {response.status}\")\r\n                    return \"\"\r\n        except Exception as e:\r\n            print(f\"Error in translation: {e}\")\r\n            return \"\"\r\n\r\n    async def translate(self, texts, original_lang, target_lang):\r\n        \"\"\"异步批量翻译\"\"\"\r\n        async with aiohttp.ClientSession() as session:\r\n            tasks = [\r\n                self.translate_single(session, text, original_lang, target_lang)\r\n                for text in texts\r\n            ]\r\n            return await asyncio.gather(*tasks)\r\n\r\n# 添加Bing翻译类\r\nclass Bing_translation:\r\n    def __init__(self):\r\n        self.session = requests.Session()\r\n        self.endpoint = \"https://www.bing.com/translator\"\r\n        self.headers = {\r\n            \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0\",\r\n        }\r\n        self.lang_map = {\"zh\": \"zh-Hans\"}\r\n\r\n    async def find_sid(self):\r\n        \"\"\"获取必要的会话参数\"\"\"\r\n        loop = asyncio.get_event_loop()\r\n        response = await loop.run_in_executor(None, lambda: self.session.get(self.endpoint, headers=self.headers))\r\n        response.raise_for_status()\r\n        url = response.url[:-10]\r\n        ig = re.findall(r\"\\\"ig\\\":\\\"(.*?)\\\"\", response.text)[0]\r\n        iid = re.findall(r\"data-iid=\\\"(.*?)\\\"\", response.text)[-1]\r\n        key, token = re.findall(\r\n            r\"params_AbusePreventionHelper\\s=\\s\\[(.*?),\\\"(.*?)\\\",\", response.text\r\n        )[0]\r\n        return url, ig, iid, key, token\r\n\r\n    async def translate_single(self, session, text, original_lang, target_lang):\r\n        \"\"\"单个文本的异步翻译\"\"\"\r\n        if not text or not text.strip():\r\n            return \"\"\r\n            \r\n        # 处理语言代码映射\r\n        lang_in = self.lang_map.get(original_lang, original_lang)\r\n        lang_out = self.lang_map.get(target_lang, target_lang)\r\n        \r\n        # 自动语言检测处理\r\n        if lang_in == \"auto\":\r\n            lang_in = \"auto-detect\"\r\n        \r\n        # Bing翻译最大长度限制\r\n        text = text[:1000]\r\n        \r\n        try:\r\n            url, ig, iid, key, token = await self.find_sid()\r\n            \r\n            # 通过异步HTTP请求执行翻译\r\n            async with session.post(\r\n                f\"{url}ttranslatev3?IG={ig}&IID={iid}\",\r\n                data={\r\n                    \"fromLang\": lang_in,\r\n                    \"to\": lang_out,\r\n                    \"text\": text,\r\n                    \"token\": token,\r\n                    \"key\": key,\r\n                },\r\n                headers=self.headers,\r\n            ) as response:\r\n                if response.status == 200:\r\n                    result = await response.json()\r\n                    return result[0][\"translations\"][0][\"text\"]\r\n                else:\r\n                    error_text = await response.text()\r\n                    print(f\"Bing翻译错误: {response.status}, 详情: {error_text}\")\r\n                    return \"\"\r\n        except Exception as e:\r\n            print(f\"Bing翻译过程中发生错误: {e}\")\r\n            return \"\"\r\n\r\n    async def translate(self, texts, original_lang, target_lang):\r\n        \"\"\"异步批量翻译\"\"\"\r\n        print(f\"开始Bing翻译，共 {len(texts)} 个文本\")\r\n        async with aiohttp.ClientSession() as session:\r\n            tasks = []\r\n            for i, text in enumerate(texts):\r\n                # 添加延迟以避免请求过快被屏蔽\r\n                await asyncio.sleep(0.5 * (i % 3))  # 每3个请求一组，每组之间间隔0.5秒\r\n                tasks.append(self.translate_single(session, text, original_lang, target_lang))\r\n            \r\n            results = await asyncio.gather(*tasks)\r\n            print(f\"Bing翻译完成，共翻译 {len(results)} 个文本\")\r\n            return results\r\n\r\nclass Deepseek_translation:\r\n    def __init__(self):\r\n        config = load_config.load_config()\r\n        self.api_key = config['translation_services']['deepseek']['auth_key']\r\n        self.url = \"https://ark.cn-beijing.volces.com/api/v3/chat/completions\"\r\n        self.headers = {\r\n            \"Authorization\": f\"Bearer {self.api_key}\",\r\n            \"Content-Type\": \"application/json\"\r\n        }\r\n        self.model = config['translation_services']['deepseek']['model_name']\r\n\r\n    async def translate_single(self, session, text, original_lang, target_lang):\r\n        \"\"\"单个文本的异步翻译\"\"\"\r\n        payload = {\r\n            \"model\": self.model,\r\n            \"messages\": [\r\n                {\r\n                    \"role\": \"system\",\r\n                    \"content\": f\"You are a professional translator. Translate from {original_lang} to {target_lang}.Return only the translations\"\r\n                },\r\n                {\r\n                    \"role\": \"user\",\r\n                    \"content\": text\r\n                }\r\n            ],\r\n            \"temperature\": 0.3,\r\n            \"top_p\": 0.9\r\n        }\r\n\r\n        try:\r\n            async with session.post(self.url, headers=self.headers, json=payload) as response:\r\n                if response.status == 200:\r\n                    result = await response.json()\r\n                    return result['choices'][0]['message']['content'].strip()\r\n                else:\r\n                    print(f\"Error: {response.status}\")\r\n                    return \"\"\r\n        except Exception as e:\r\n            print(f\"Error in translation: {e}\")\r\n            return \"\"\r\n\r\n    async def translate(self, texts, original_lang, target_lang):\r\n        \"\"\"异步批量翻译\"\"\"\r\n        async with aiohttp.ClientSession() as session:\r\n            tasks = [\r\n                self.translate_single(session, text, original_lang, target_lang)\r\n                for text in texts\r\n            ]\r\n            return await asyncio.gather(*tasks)\r\nclass Doubao_translation:\r\n    def __init__(self):\r\n        config = load_config.load_config()\r\n        self.api_key = config['translation_services']['Doubao']['auth_key']\r\n        self.url = \"https://ark.cn-beijing.volces.com/api/v3/chat/completions\"\r\n        self.headers = {\r\n            \"Authorization\": f\"Bearer {self.api_key}\",\r\n            \"Content-Type\": \"application/json\"\r\n        }\r\n        self.model = config['translation_services']['Doubao']['model_name']\r\n\r\n    async def translate_single(self, session, text, original_lang, target_lang):\r\n        \"\"\"单个文本的异步翻译\"\"\"\r\n        payload = {\r\n            \"model\": self.model,\r\n            \"messages\": [\r\n                {\r\n                    \"role\": \"system\",\r\n                    \"content\": f\"Translate from {original_lang} to {target_lang}. Return ONLY the translation. No explanations. No notes. No annotations.\"\r\n                },\r\n                {\r\n                    \"role\": \"user\",\r\n                    \"content\": text\r\n                }\r\n            ],\r\n            \"temperature\": 0.3,\r\n            \"top_p\": 0.9\r\n        }\r\n\r\n        try:\r\n            async with session.post(self.url, headers=self.headers, json=payload) as response:\r\n                if response.status == 200:\r\n                    result = await response.json()\r\n                    return result['choices'][0]['message']['content'].strip()\r\n                else:\r\n                    print(f\"Error: {response.status}\")\r\n                    return \"\"\r\n        except Exception as e:\r\n            print(f\"Error in translation: {e}\")\r\n            return \"\"\r\n\r\n    async def translate(self, texts, original_lang, target_lang):\r\n        \"\"\"异步批量翻译\"\"\"\r\n        async with aiohttp.ClientSession() as session:\r\n            tasks = [\r\n                self.translate_single(session, text, original_lang, target_lang)\r\n                for text in texts\r\n            ]\r\n            return await asyncio.gather(*tasks)\r\nclass Qwen_translation:\r\n    def __init__(self):\r\n        config = load_config.load_config()\r\n        self.api_key = config['translation_services']['Qwen']['auth_key']\r\n        self.url = \"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions\"\r\n        self.headers = {\r\n            \"Authorization\": f\"Bearer {self.api_key}\",\r\n            \"Content-Type\": \"application/json\"\r\n        }\r\n        self.model = config['translation_services']['Qwen']['model_name']\r\n\r\n    async def translate_single(self, session, text, original_lang, target_lang):\r\n        \"\"\"单个文本的异步翻译\"\"\"\r\n        payload = {\r\n            \"model\": self.model,\r\n            \"messages\": [\r\n                {\r\n                    \"role\": \"system\",\r\n                    \"content\": f\"You are a professional translator. Translate from {original_lang} to {target_lang}.Return only the translations\"\r\n                },\r\n                {\r\n                    \"role\": \"user\",\r\n                    \"content\": text\r\n                }\r\n            ],\r\n            \"temperature\": 0.3,\r\n            \"top_p\": 0.9\r\n        }\r\n\r\n        try:\r\n            async with session.post(self.url, headers=self.headers, json=payload) as response:\r\n                if response.status == 200:\r\n                    result = await response.json()\r\n                    return result['choices'][0]['message']['content'].strip()\r\n                else:\r\n                    print(f\"Error: {response.status}\")\r\n                    return \"\"\r\n        except Exception as e:\r\n            print(f\"Error in translation: {e}\")\r\n            return \"\"\r\n\r\n    async def translate(self, texts, original_lang, target_lang):\r\n        \"\"\"异步批量翻译\"\"\"\r\n        async with aiohttp.ClientSession() as session:\r\n            tasks = [\r\n                self.translate_single(session, text, original_lang, target_lang)\r\n                for text in texts\r\n            ]\r\n            return await asyncio.gather(*tasks)\r\n\r\nclass Grok_translation:\r\n    def __init__(self):\r\n        config = load_config.load_config()\r\n        # 修改键名为大写的'Grok'以匹配其他API命名风格\r\n        self.api_key = config['translation_services']['Grok']['auth_key']\r\n        self.url = \"https://api-proxy.me/xai/v1/chat/completions\"\r\n        self.headers = {\r\n            \"Content-Type\": \"application/json\",\r\n            \"X-Api-Key\": self.api_key,\r\n            \"Authorization\": f\"Bearer {self.api_key}\"\r\n        }\r\n        self.model = config['translation_services']['Grok']['model_name']\r\n\r\n    async def translate_single(self, session, text, original_lang, target_lang):\r\n        \"\"\"单个文本的异步翻译\"\"\"\r\n        payload = {\r\n            \"model\": self.model,\r\n            \"messages\": [\r\n                {\r\n                    \"role\": \"system\",\r\n                    \"content\": f\"You are a professional translator. Translate from {original_lang} to {target_lang}. Return ONLY the translation without explanations or notes.\"\r\n                },\r\n                {\r\n                    \"role\": \"user\",\r\n                    \"content\": text\r\n                }\r\n            ],\r\n            \"temperature\": 0.3,\r\n            \"stream\": False\r\n        }\r\n\r\n        try:\r\n            async with session.post(self.url, headers=self.headers, json=payload) as response:\r\n                if response.status == 200:\r\n                    result = await response.json()\r\n                    return result['choices'][0]['message']['content'].strip()\r\n                else:\r\n                    print(f\"Error: {response.status}\")\r\n                    return \"\"\r\n        except Exception as e:\r\n            print(f\"Error in translation: {e}\")\r\n            return \"\"\r\n\r\n    async def translate(self, texts, original_lang, target_lang):\r\n        \"\"\"异步批量翻译\"\"\"\r\n        async with aiohttp.ClientSession() as session:\r\n            tasks = [\r\n                self.translate_single(session, text, original_lang, target_lang)\r\n                for text in texts\r\n            ]\r\n            return await asyncio.gather(*tasks)\r\n\r\nclass ThirdParty_translation:\r\n    def __init__(self):\r\n        config = load_config.load_config()\r\n        self.api_key = config['translation_services']['ThirdParty']['auth_key']\r\n        self.url = config['translation_services']['ThirdParty']['api_url']\r\n        self.headers = {\r\n            \"Authorization\": f\"Bearer {self.api_key}\",\r\n            \"Content-Type\": \"application/json\"\r\n        }\r\n        self.model = config['translation_services']['ThirdParty']['model_name']\r\n\r\n    async def translate_single(self, session, text, original_lang, target_lang):\r\n        \"\"\"单个文本的异步翻译\"\"\"\r\n        payload = {\r\n            \"model\": self.model,\r\n            \"messages\": [\r\n                {\r\n                    \"role\": \"system\",\r\n                    \"content\": f\"You are a professional translator. Translate from {original_lang} to {target_lang}. Return ONLY the translation without explanations or notes.\"\r\n                },\r\n                {\r\n                    \"role\": \"user\",\r\n                    \"content\": text\r\n                }\r\n            ],\r\n            \"temperature\": 0.3,\r\n            \"stream\": False\r\n        }\r\n\r\n        try:\r\n            async with session.post(self.url, headers=self.headers, json=payload) as response:\r\n                if response.status == 200:\r\n                    result = await response.json()\r\n                    translated_text = result['choices'][0]['message']['content'].strip()\r\n                    # 添加调试日志\r\n                    print(f\"ThirdParty translated: '{text[:30]}...' -> '{translated_text[:30]}...'\")\r\n                    return translated_text\r\n                else:\r\n                    error_text = await response.text()\r\n                    print(f\"Error: {response.status}, Details: {error_text}\")\r\n                    return \"\"\r\n        except Exception as e:\r\n            print(f\"Error in translation: {e}\")\r\n            return \"\"\r\n\r\n    async def translate(self, texts, original_lang, target_lang):\r\n        \"\"\"异步批量翻译\"\"\"\r\n        print(f\"Starting ThirdParty translation of {len(texts)} texts\")\r\n        async with aiohttp.ClientSession() as session:\r\n            tasks = [\r\n                self.translate_single(session, text, original_lang, target_lang)\r\n                for text in texts\r\n            ]\r\n            results = await asyncio.gather(*tasks)\r\n            print(f\"ThirdParty translation completed, {len(results)} texts translated\")\r\n            return results\r\n\r\nclass GLM_translation:\r\n    def __init__(self):\r\n        config = load_config.load_config()\r\n        self.api_key = config['translation_services']['GLM']['auth_key']\r\n        self.url = \"https://open.bigmodel.cn/api/paas/v4/chat/completions\"\r\n        self.headers = {\r\n            \"Authorization\": f\"Bearer {self.api_key}\",\r\n            \"Content-Type\": \"application/json\"\r\n        }\r\n        self.model = config['translation_services']['GLM']['model_name']\r\n\r\n    async def translate_single(self, session, text, original_lang, target_lang):\r\n        \"\"\"单个文本的异步翻译\"\"\"\r\n        payload = {\r\n            \"model\": self.model,\r\n            \"messages\": [\r\n                {\r\n                    \"role\": \"system\",\r\n                    \"content\": f\"You are a professional translator. Translate from {original_lang} to {target_lang}. Return ONLY the translation without explanations or notes.\"\r\n                },\r\n                {\r\n                    \"role\": \"user\",\r\n                    \"content\": text\r\n                }\r\n            ],\r\n            \"temperature\": 0.3,\r\n            \"top_p\": 0.7,\r\n            \"do_sample\": False\r\n        }\r\n\r\n        try:\r\n            async with session.post(self.url, headers=self.headers, json=payload) as response:\r\n                if response.status == 200:\r\n                    result = await response.json()\r\n                    return result['choices'][0]['message']['content'].strip()\r\n                else:\r\n                    error_text = await response.text()\r\n                    print(f\"Error: {response.status}, Details: {error_text}\")\r\n                    return \"\"\r\n        except Exception as e:\r\n            print(f\"Error in GLM translation: {e}\")\r\n            return \"\"\r\n\r\n    async def translate(self, texts, original_lang, target_lang):\r\n        \"\"\"异步批量翻译\"\"\"\r\n        print(f\"Starting GLM translation of {len(texts)} texts\")\r\n        async with aiohttp.ClientSession() as session:\r\n            tasks = [\r\n                self.translate_single(session, text, original_lang, target_lang)\r\n                for text in texts\r\n            ]\r\n            results = await asyncio.gather(*tasks)\r\n            print(f\"GLM translation completed, {len(results)} texts translated\")\r\n            return results\r\n\r\n# 测试代码\r\nasync def main():\r\n    texts = [\r\n        \"Hello, how are you?\",\r\n        \"What's the weather like today?\",\r\n        \"I love programming\",\r\n        \"Python is awesome\",\r\n        \"Machine learning is interesting\"\r\n    ]\r\n\r\n    # OpenAI翻译测试\r\n    openai_translator = Openai_translation(\"gpt-3.5-turbo\")\r\n    translated_openai = await openai_translator.translate(\r\n        texts=texts,\r\n        original_lang=\"en\",\r\n        target_lang=\"zh\"\r\n    )\r\n    print(\"OpenAI translations:\")\r\n    for src, tgt in zip(texts, translated_openai):\r\n        print(f\"{src} -> {tgt}\")\r\n\r\n    # DeepSeek翻译测试\r\n    deepseek_translator = Deepseek_translation()\r\n    translated_deepseek = await deepseek_translator.translate(\r\n        texts=texts,\r\n        original_lang=\"en\",\r\n        target_lang=\"zh\"\r\n    )\r\n    print(\"\\nDeepSeek translations:\")\r\n    for src, tgt in zip(texts, translated_deepseek):\r\n        print(f\"{src} -> {tgt}\")\r\n\r\n    qwen_translator = Qwen_translation()\r\n    translated_deepseek = await qwen_translator.translate(\r\n        texts=texts,\r\n        original_lang=\"en\",\r\n        target_lang=\"zh\"\r\n    )\r\n    print(\"\\nQwen translations:\")\r\n    for src, tgt in zip(texts, translated_deepseek):\r\n        print(f\"{src} -> {tgt}\")\r\n\r\n    # 添加Grok翻译测试\r\n    grok_translator = Grok_translation()\r\n    translated_grok = await grok_translator.translate(\r\n        texts=texts,\r\n        original_lang=\"en\",\r\n        target_lang=\"zh\"\r\n    )\r\n    print(\"\\nGrok translations:\")\r\n    for src, tgt in zip(texts, translated_grok):\r\n        print(f\"{src} -> {tgt}\")\r\n\r\n    # 添加ThirdParty翻译测试\r\n    try:\r\n        thirdparty_translator = ThirdParty_translation()\r\n        translated_thirdparty = await thirdparty_translator.translate(\r\n            texts=texts,\r\n            original_lang=\"en\",\r\n            target_lang=\"zh\"\r\n        )\r\n        print(\"\\nThirdParty translations:\")\r\n        for src, tgt in zip(texts, translated_thirdparty):\r\n            print(f\"{src} -> {tgt}\")\r\n    except Exception as e:\r\n        print(f\"Error testing ThirdParty translation: {e}\")\r\n\r\n    # 添加 GLM 翻译测试\r\n    try:\r\n        glm_translator = GLM_translation()\r\n        translated_glm = await glm_translator.translate(\r\n            texts=texts,\r\n            original_lang=\"en\",\r\n            target_lang=\"zh\"\r\n        )\r\n        print(\"\\nGLM translations:\")\r\n        for src, tgt in zip(texts, translated_glm):\r\n            print(f\"{src} -> {tgt}\")\r\n    except Exception as e:\r\n        print(f\"Error testing GLM translation: {e}\")\r\n\r\nif __name__ == \"__main__\":\r\n    asyncio.run(main())\r\n"
  },
  {
    "path": "EbookTranslator/EbookTranslator/YouDao_translation.py",
    "content": "import uuid\nimport requests\nimport hashlib\nimport time\nimport json\n\n\ndef translate(texts,original_lang, target_lang):\n    \"\"\"\n    有道翻译API接口\n\n    参数:\n    texts: list, 要翻译的文本列表\n    target_lang: str, 目标语言代码\n    credentials: dict, 包含 app_key 和 app_secret 的字典\n\n    返回:\n    list: 翻译后的文本列表\n    \"\"\"\n    YOUDAO_URL = 'https://openapi.youdao.com/v2/api'\n\n    with open(\"config.json\", 'r', encoding='utf-8') as f:\n        config = json.load(f)\n\n    # 获取指定服务的认证信息\n    if target_lang == 'zh':\n        target_lang='zh-CHS'\n    service_name = \"youdao\"\n    credentials = config['translation_services'].get(service_name)\n    if not credentials:\n        raise ValueError(f\"Translation service '{service_name}' not found in config\")\n\n\n    def encrypt(sign_str):\n        hash_algorithm = hashlib.sha256()\n        hash_algorithm.update(sign_str.encode('utf-8'))\n        return hash_algorithm.hexdigest()\n\n    def truncate(q):\n        if q is None:\n            return None\n        size = len(q)\n        return q if size <= 20 else q[0:10] + str(size) + q[size - 10:size]\n\n    def do_request(data):\n        headers = {'Content-Type': 'application/x-www-form-urlencoded'}\n        return requests.post(YOUDAO_URL, data=data, headers=headers)\n\n    try:\n        # 确保输入文本为列表格式\n        if isinstance(texts, str):\n            texts = [texts]\n\n        print(type(texts))\n\n        # 准备请求数据\n        data = {\n            'from': original_lang,\n            'to': target_lang,\n            'signType': 'v3',\n            'curtime': str(int(time.time())),\n            'appKey': credentials['app_key'],\n            'q': texts,\n            'salt': str(uuid.uuid1()),\n            'vocabId': \"您的用户词表ID\"\n        }\n\n        # 生成签名\n        sign_str = (credentials['app_key'] +\n                    truncate(''.join(texts)) +\n                    data['salt'] +\n                    data['curtime'] +\n                    credentials['app_secret'])\n        data['sign'] = encrypt(sign_str)\n\n        # 发送请求\n        response = do_request(data)\n        response_data = json.loads(response.content.decode(\"utf-8\"))\n\n        # 提取翻译结果\n        translations = [result[\"translation\"] for result in response_data[\"translateResults\"]]\n        print(translations)\n        return translations\n\n    except Exception as e:\n        print(f\"翻译出错: {str(e)}\")\n        return None\n# 使用示例:\nif __name__ == '__main__':\n    # 认证信息\n\n\n    # 要翻译的文本\n    texts = [\"hello\", '待输入的文字\"2', \"待输入的文字3\"]\n    original_lang = 'auto'\n\n    # 目标语言\n    target_lang = 'zh'\n\n    # 调用翻译\n    results = translate(texts,original_lang='auto', target_lang=target_lang)\n    print(results,'ggg')\n\n    if results:\n        for original, translated in zip(texts, results):\n            print(f\"原文: {original}\")\n            print(f\"译文: {translated}\\n\")\n\n"
  },
  {
    "path": "EbookTranslator/EbookTranslator/__init__.py",
    "content": "\"\"\"\nEbookTranslator - 世界上性能最高的电子书保留布局翻译库\n The world's highest performing e-book retention layout translation library\n\"\"\"\n\n__version__ = '0.1.0'\n\nfrom .main_function import main_function\n\n# 导出主要类和函数\n__all__ = ['main_function']\n"
  },
  {
    "path": "EbookTranslator/EbookTranslator/cli.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nEbookTranslator的命令行界面\n\"\"\"\n\nimport argparse\nimport sys\nimport os\nfrom pathlib import Path\nfrom .main_function import main_function\n\n\ndef main():\n    \"\"\"命令行入口点\"\"\"\n    parser = argparse.ArgumentParser(description='翻译PDF文档')\n    parser.add_argument('pdf_path', type=str, help='PDF文件路径')\n    parser.add_argument('-o', '--original', default='auto', help='原始语言 (默认: auto)')\n    parser.add_argument('-t', '--target', default='zh', help='目标语言 (默认: zh)')\n    parser.add_argument('-b', '--begin', type=int, default=1, help='开始页码 (默认: 1)')\n    parser.add_argument('-e', '--end', type=int, default=None, help='结束页码 (默认: 最后一页)')\n    parser.add_argument('-c', '--config', type=str, default=None, help='配置文件路径')\n    parser.add_argument('-d', '--dpi', type=int, default=72, help='OCR模式的DPI (默认: 72)')\n\n    args = parser.parse_args()\n\n    # 检查PDF文件是否存在\n    print('路径',args.pdf_path)\n\n    if not os.path.exists(args.pdf_path):\n        print(f\"错误: 找不到文件 '{args.pdf_path}'\")\n        sys.exit(1)\n\n    try:\n        # 运行主函数\n        translator = main_function(\n            pdf_path=args.pdf_path,\n            original_language=args.original,\n            target_language=args.target,\n            bn=args.begin,\n            en=args.end,\n            config_path=args.config,\n            DPI=args.dpi\n        )\n        translator.main()\n        print(f\"翻译完成! 输出文件保存在 target 目录\")\n    except Exception as e:\n        print(f\"翻译过程中发生错误: {e}\")\n        sys.exit(1)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "EbookTranslator/EbookTranslator/convert2pdf.py",
    "content": "import fitz\nimport os\n\n\ndef convert_to_pdf(input_file, output_file=None):\n    \"\"\"\n    将支持的文档格式转换为 PDF，支持跨平台路径处理\n\n    Args:\n        input_file (str): 输入文件的完整路径\n        output_file (str, optional): 输出PDF文件的完整路径。如果为None，则使用输入文件名+.pdf\n\n    Returns:\n        bool: 转换是否成功\n    \"\"\"\n    try:\n        # 规范化路径，处理不同平台的路径分隔符\n        input_file = os.path.normpath(input_file)\n\n        if not os.path.exists(input_file):\n            print(f\"错误：输入文件 '{input_file}' 不存在\")\n            return False\n\n        # 如果未指定输出文件，则基于输入文件生成输出路径\n        if output_file is None:\n            # 获取文件名和目录\n            file_dir = os.path.dirname(input_file)\n            file_name = os.path.basename(input_file)\n            name_without_ext = os.path.splitext(file_name)[0]\n\n            # 在同一目录下创建同名PDF文件\n            output_file = os.path.join(file_dir, f\"{name_without_ext}.pdf\")\n\n        # 确保输出目录存在\n        output_dir = os.path.dirname(output_file)\n        if output_dir and not os.path.exists(output_dir):\n            os.makedirs(output_dir, exist_ok=True)\n\n        print(f\"正在处理文件: {input_file}\")\n        print(f\"输出文件将保存为: {output_file}\")\n\n        # 1. 先用 fitz.open 打开文档（EPUB、XPS、FB2 等格式）\n        doc = fitz.open(input_file)\n        print(f\"文档页数: {len(doc)}\")\n\n        # 2. 调用 convert_to_pdf() 得到 PDF 格式字节流\n        pdf_bytes = doc.convert_to_pdf()\n\n        # 3. 再以 \"pdf\" 格式打开这段字节流\n        pdf_doc = fitz.open(\"pdf\", pdf_bytes)\n\n        # 4. 保存为真正的 PDF 文件\n        pdf_doc.save(output_file)\n\n        # 关闭文档\n        pdf_doc.close()\n        doc.close()\n\n        # 检查输出文件是否成功创建\n        if os.path.exists(output_file):\n            print(f\"转换成功！PDF文件已保存为: {output_file}\")\n            return True\n        else:\n            print(\"转换似乎完成，但输出文件未找到\")\n            return False\n\n    except fitz.FileDataError as e:\n        print(f\"文件格式错误或文件损坏：{str(e)}\")\n    except PermissionError as e:\n        print(f\"权限错误：无法访问或写入文件 - {str(e)}\")\n    except Exception as e:\n        print(f\"转换失败，错误类型: {type(e).__name__}\")\n        print(f\"错误详情: {str(e)}\")\n        # 在调试模式下打印完整的堆栈跟踪\n        import traceback\n        traceback.print_exc()\n\n    return False\n# 使用示例\nif __name__ == \"__main__\":\n    # 单个文件转换示例\n    input_file = \"666 (1).epub\"\n\n    # 验证文件扩展名\n    if not input_file.lower().endswith(('.xps', '.epub', '.fb2', '.cbz', '.mobi')):\n        print(f\"不支持的文件格式。支持的格式包括: XPS, EPUB, FB2, CBZ, MOBI\")\n    else:\n        convert_to_pdf(input_file)\n\n    # 批量转换示例\n    # input_directory = \"documents\"\n    # batch_convert_to_pdf(input_directory)\n"
  },
  {
    "path": "EbookTranslator/EbookTranslator/load_config.py",
    "content": "import os\nimport json\nimport requests\nfrom pathlib import Path\nfrom typing import Optional, Dict\n\n\ndef get_working_dir() -> Path:\n    \"\"\"\n    获取工作目录\n    返回当前工作目录（即命令行执行目录或调用脚本所在目录）\n    \"\"\"\n    return Path.cwd()\n\n\n# 定义应用数据目录\nWORKING_DIR = get_working_dir()\nAPP_DATA_DIR = WORKING_DIR  # 示例：将 APP_DATA_DIR 定义为工作目录\nprint(f\"Working directory: {WORKING_DIR}\")\n\n\ndef resolve_path(path: str) -> Path:\n    \"\"\"\n    解析路径，支持绝对路径、相对路径和文件名。\n\n    Args:\n        path (str): 输入路径，可以是绝对路径、相对路径或文件名。\n\n    Returns:\n        Path: 解析后的完整路径。\n    \"\"\"\n    # 如果 path 是绝对路径，直接返回\n    if Path(path).is_absolute():\n        return Path(path)\n\n    # 如果 path 是相对路径或文件名，与 APP_DATA_DIR 拼接\n    return APP_DATA_DIR / path\n\n\ndef load_config(config_path: Optional[str] = None) -> Optional[Dict]:\n    \"\"\"\n    加载主配置文件，优先使用传入的 config_path 路径。\n    如果未传入或路径无效，则尝试使用 APP_DATA_DIR 中的文件。\n    如果 APP_DATA_DIR 中也没有 config.json，则从指定 URL 下载。\n\n    Args:\n        config_path (Optional[str]): 配置文件路径，可以是绝对路径、相对路径或文件名。\n\n    Returns:\n        Dict: 配置数据，如果加载失败则返回 None。\n    \"\"\"\n    try:\n        # 如果传入了 config_path 参数，优先使用\n        if config_path:\n            config_path = resolve_path(config_path)  # 解析路径\n            if config_path.exists():\n                with config_path.open(\"r\", encoding=\"utf-8\") as f:\n                    return json.load(f)\n            else:\n                print(f\"Specified config path does not exist: {config_path}\")\n\n        # 如果没有传入 config_path 或路径无效，则使用 APP_DATA_DIR 中的 config.json\n        app_config_path = APP_DATA_DIR / \"config.json\"\n        if app_config_path.exists():\n            with app_config_path.open(\"r\", encoding=\"utf-8\") as f:\n                return json.load(f)\n        else:\n            # 如果 APP_DATA_DIR 中没有，则尝试从指定 URL 下载 config.json\n            url = \"https://raw.githubusercontent.com/CBIhalsen/PolyglotPDF/refs/heads/main/config.json\"\n            response = requests.get(url, timeout=20)\n            if response.status_code == 200:\n                # 将下载的内容保存到 APP_DATA_DIR\n                APP_DATA_DIR.mkdir(parents=True, exist_ok=True)  # 确保 APP_DATA_DIR 存在\n                print(\n                    f\"config.json file not found, downloading config.json from: {url}\"\n                )\n                with app_config_path.open(\"w\", encoding=\"utf-8\") as f:\n                    f.write(response.text)\n                return response.json()\n            else:\n                print(f\"Failed to download config.json, HTTP status code: {response.status_code}\")\n                return None\n    except Exception as e:\n        print(f\"Error loading config: {str(e)}\")\n        return None\n\n\ndef get_file_path(filename: str) -> Path:\n    \"\"\"\n    获取配置文件的完整路径，优先使用 APP_DATA_DIR 中的文件。\n\n    Args:\n        filename (str): 配置文件名。\n\n    Returns:\n        Path: 配置文件的完整路径。\n    \"\"\"\n    # 首先检查 APP_DATA_DIR 中是否有该文件\n    app_data_file = APP_DATA_DIR / filename\n    if app_data_file.exists():\n        return app_data_file\n\n    # 如果 APP_DATA_DIR 中没有，则使用当前脚本所在目录的文件\n    return Path(__file__).parent / filename\n"
  },
  {
    "path": "EbookTranslator/EbookTranslator/main_function.py",
    "content": "import math\nfrom . import All_Translation as at\nfrom PIL import Image\nimport pytesseract\nimport time\nimport fitz\nimport os\nimport unicodedata\nfrom pathlib import Path\nfrom . import load_config\n\nimport re\nfrom .convert2pdf import convert_to_pdf\nfrom .load_config import APP_DATA_DIR\n\n# print(use_mupdf,'mupdf值')\n# print('当前',config['count'])\n\n\n\ndef get_font_by_language(target_language):\n    font_mapping = {\n        'zh': \"'Microsoft YaHei', 'SimSun'\",  # 中文\n        'en': \"'Times New Roman', Arial\",      # 英文\n        'ja': \"'MS Mincho', 'Yu Mincho'\",     # 日文\n        'ko': \"'Malgun Gothic'\",              # 韩文\n    }\n    # 如果找不到对应语言，返回默认字体\n    return font_mapping.get(target_language, \"'Times New Roman', Arial\")\n\n\ndef is_math(text,pag_num,font_info):\n    \"\"\"\n    判断文本是否为非文本（如数学公式或者长度小于4的文本）\n    \"\"\"\n\n\n    # 判断文本长度\n    # print('文本为:',text)\n    text_len = len(text)\n    if text_len < 4:\n        return True\n    math_fonts = [\n        # Computer Modern Math\n        'CMMI', 'CMSY', 'CMEX',\n        'CMMI5', 'CMMI6', 'CMMI7', 'CMMI8', 'CMMI9', 'CMMI10',\n        'CMSY5', 'CMSY6', 'CMSY7', 'CMSY8', 'CMSY9', 'CMSY10',\n\n        # AMS Math\n        'MSAM', 'MSBM', 'EUFM', 'EUSM',\n\n        # Times/Palatino Math\n        'TXMI', 'TXSY', 'PXMI', 'PXSY',\n\n        # Modern Math\n        'CambriaMath', 'AsanaMath', 'STIXMath', 'XitsMath',\n        'Latin Modern Math', 'Neo Euler'\n    ]\n    # 检查文本长度是否小于50且字体是否在数学字体列表中\n    text_len_non_sp = len(text.replace(\" \", \"\"))\n\n    if text_len < 70 and any(math_font in font_info for math_font in math_fonts):\n        # print(text,'小于50且字体')\n        return True\n\n    if 15 < text_len_non_sp <100:\n        # 使用正则表达式找出所有5个或更多任意字符连续组成的单词\n        long_words = re.findall(r'\\S{5,}', text)\n        if len(long_words) < 2:\n            # print(text_len)\n            # print(text, '15 < text_len <100')\n            return True\n\n\n    # 分行处理\n    lines = text.split('\\n')\n    len_lines = len([line for line in lines if line.strip()])\n\n    # 找到长度最小和最大的行\n    min_line_len = min((len(line) for line in lines if line.strip()), default=text_len)\n    max_line_len = max((len(line) for line in lines), default=text_len)\n\n    # 计算空格比例\n    newline_count = text.count('\\n')\n    total_spaces = text.count(' ') + (newline_count * 5)\n    space_ratio = total_spaces / text_len if text_len > 0 else 0\n\n    # 定义数学符号集合\n    math_symbols = \"=∑θ∫∂√±ΣΠfδλσε∋∈µ→()|−ˆ,.+*/[]{}^_<>~#%&@!?;:'\\\"\\\\-\"\n\n    # 检查是否存在完整单词(5个或更多非数学符号的连续字符)\n    text_no_spaces = text.replace(\" \", \"\")\n\n    # 创建一个正则表达式，匹配5个或更多连续的非数学符号字符\n    pattern = r'[^' + re.escape(math_symbols) + r']{5,}'\n    has_complete_word = bool(re.search(pattern, text_no_spaces))\n\n    # 如果没有完整单词，认为是非文本\n    if not has_complete_word:\n        # print(text, '没有完整单词')\n        return True\n\n\n    # 计算数字占比\n    digit_count = sum(c.isdigit() for c in text)\n    digit_ratio = digit_count / text_len if text_len > 0 else 0\n\n    # 如果数字占比超过30%，返回True\n    if digit_ratio > 0.3:\n        # print(text, '数字占比超过30%')\n        return True\n\n\n\n\n\n    # 检查数学公式\n    math_symbols = set(\"=∑θ∫∂√±ΣΠδλσε∋∈µ→()|−ˆ,...\")\n    # 数学公式判断条件2:包含至少2个数学符号且总文本较短\n    if sum(1 for sym in math_symbols if sym in text) >= 2 and len(text_no_spaces) < 25:\n        # found_symbols = [sym for sym in text if sym in math_symbols]\n        # print(f\"在文本中找到的数学符号: {found_symbols}\")\n        # print(sum(1 for sym in math_symbols if sym in text))\n        # print(text, '条件2')\n        return True\n\n    # 数学公式判断条件1:包含至少2个数学符号且行短且行数少且最大行长度小\n    if sum(1 for sym in math_symbols if sym in text) >= 2 and min_line_len < 10 and len_lines < 5 and max_line_len < 35:\n        # print(text, '条件1')\n        return True\n\n    # 数学公式判断条件3:包含至少2个数学符号且空格比例高\n    if sum(1 for sym in math_symbols if sym in text) >= 2 and space_ratio > 0.5:\n        # print(text, '条件3')\n        return True\n    # 数学公式判断条件4:包含至少1个数学符号且空格数高\n    if sum(1 for sym in math_symbols if sym in text) >= 1 and total_spaces > 3 and text_len<20:\n        # print(text, '条件4')\n        return True\n\n    return False\n\ndef line_non_text(text):\n    \"\"\"\n    判断文本是否由纯数字和所有(Unicode)标点符号组成\n    参数：\n        text: 待检查的文本\n    返回：\n        bool: 如果文本由纯数字和标点符号组成返回True，否则返回False\n    \"\"\"\n    text = text.strip()\n    if not text:\n        return False\n    for ch in text:\n        # 使用 unicodedata.category() 获取字符在 Unicode 标准中的分类\n        # 'Nd' 代表十进制数字 (Number, Decimal digit)\n        # 'P' 代表各种标点 (Punctuation)，如 Po, Ps, Pe, 等\n        cat = unicodedata.category(ch)\n        if not (cat == 'Nd' or cat.startswith('P')):\n            return False\n    return True\n\n\ndef is_non_text(text):\n    \"\"\"\n    判断是否为参考文献格式\n    参数：\n    text: 待检查的文本\n    返回：\n    bool: 如果是参考文献格式返回True，否则返回False\n    \"\"\"\n    # 去除开头的空白字符\n    text = text.lstrip()\n\n    # 检查是否以[数字]开头\n    pattern = r'^\\[\\d+\\]'\n\n    if re.match(pattern, text):\n        return True\n\n    return False\nfont_collection = []\n\n\nclass main_function:\n    def __init__(self, pdf_path,\n                 original_language, target_language,bn = None,en = None,config_path = None,\n                 DPI=72,):\n        \"\"\"\n        这里的参数与原来保持一致或自定义。主要多加一个 self.pages_data 用于存储所有页面的提取结果。\n        \"\"\"\n        self.config = self.get_config(config_path)\n        config = self.config\n\n\n\n        self.translation_type = config['default_services']['Translation_api']\n        self.translation = config['default_services']['Enable_translation']\n        self.use_mupdf = not config['default_services']['ocr_model']\n\n        self.PPC = config['PPC']\n        print('ppc',self.PPC)\n        self.line_model = config['default_services']['line_model']\n        print('line', self.line_model)\n\n        self.pdf_path = pdf_path\n        self.full_path = self.resolve_path(pdf_path)\n        self.doc = fitz.open(self.full_path)\n\n        self.original_language = original_language\n        self.target_language = target_language\n        self.DPI = DPI\n        self.bn = bn-1\n        self.en = en-1\n\n        self.t = time.time()\n        self.pdf_basename = os.path.basename(self.pdf_path)  # g2.pdf\n        pdf_name_only, _ = os.path.splitext(self.pdf_basename)  # g2\n        self.pdf_name = pdf_name_only\n        # 新增一个全局列表，用于存所有页面的 [文本, bbox]，以及翻译后结果\n        # 形式: self.pages_data[page_index] = [ [原文, bbox], [原文, bbox], ... ]\n        self.pages_data = []\n    @staticmethod\n    def resolve_path(pdf_path: str) -> str:\n        \"\"\"\n        解析 PDF 文件路径，支持绝对路径、相对路径和文件名。\n\n        Args:\n            pdf_path (str): 输入路径，可以是绝对路径、相对路径或文件名。\n\n        Returns:\n            str: 解析后的绝对路径。\n        \"\"\"\n        path = Path(pdf_path)\n\n        # 如果是绝对路径，直接返回\n        if path.is_absolute():\n            return str(path)\n\n        # 如果是相对路径或文件名，与 APP_DATA_DIR 拼接\n        resolved_path = APP_DATA_DIR / path\n\n        # 返回解析后的绝对路径\n        return str(resolved_path.resolve())\n\n    def get_config(self,config_path=None):\n        config = load_config.load_config(config_path)\n\n\n        return config\n\n    def main(self):\n        \"\"\"\n        主流程函数。只做“计数更新、生成缩略图、建条目”等老逻辑，替换原来在这里的逐页翻译写入。\n        但是保留 if use_mupdf: for... self.start(...) else: for... self.start(...)\n        不做“翻译和写入”的动作，而是只做“提取文本”。\n        提取完所有页面后，批量翻译，再统一写入 PDF。\n        \"\"\"\n\n\n\n        file_extension = os.path.splitext(self.pdf_basename)[1].lower()\n\n\n        # 使用APP_DATA_DIR构建上传目录路径\n\n\n\n\n        # 如果不是PDF，进行转换\n        if file_extension != '.pdf':\n            # 创建PDF文件名\n            pdf_filename = os.path.splitext(self.pdf_basename)[0] + '.pdf'\n            pdf_file_path = os.path.join(APP_DATA_DIR, pdf_filename)\n\n            # 转换文件\n            if convert_to_pdf(input_file=self.full_path, output_file=pdf_file_path):\n                # 转换成功后删除原始文件\n                print('convert success')\n\n\n        # 4. 保留原先判断是否 use_mupdf 的代码，以便先提取文本\n        page_count =self.doc.page_count\n        if self.bn == None or self.bn <0:\n            self.bn = 0\n        if self.en == None or self.en <0:\n            self.en = page_count\n        if self.en < self.bn:\n            self.bn, self.en = self.en, self.bn\n\n        if self.use_mupdf:\n            start_page = self.bn\n            end_page = min(self.en, page_count)\n\n            # 使用 PyMuPDF 直接获取文本块\n            for i in range(start_page, end_page):\n                self.start(image=None, pag_num=i)  # 只做提取，不做翻译写入\n        else:\n            # OCR 模式\n            zoom = self.DPI / 72\n            mat = fitz.Matrix(zoom, zoom)\n            # 处理从 self.bn 到 self.en 的页面范围，并确保 self.en 不超过文档页数\n            start_page = self.bn\n            end_page = min(self.en, page_count)\n\n            # 迭代指定范围的页面\n            for i in range(start_page, end_page):\n                page = self.doc[i]  # 获取指定页面\n                pix = page.get_pixmap(matrix=mat)\n                image = Image.frombytes(\"RGB\", [pix.width, pix.height], pix.samples)\n                # 如果需要保存图像到文件，可自行保留或注释\n                # image.save(f'page_{i}.jpg', 'JPEG')\n                self.start(image=image, pag_num=i)  # 只做提取，不做翻译写入\n\n        # 5. 若开启翻译，则批量翻译所有提取的文本\n\n        self.batch_translate_pages_data(\n                original_language=self.original_language,\n                target_language=self.target_language,\n                translation_type=self.translation_type,\n                batch_size=self.PPC\n            )\n\n        # 6. 将翻译结果统一写入 PDF（覆盖+插入译文）\n        self.apply_translations_to_pdf()\n\n        # 7. 保存 PDF、更新状态\n\n        # 定义目标文件夹路径\n        target_folder = Path(APP_DATA_DIR) / \"target\"\n\n        # 如果目标文件夹不存在，则创建\n        target_folder.mkdir(parents=True, exist_ok=True)\n        # 定义目标 PDF 文件路径\n        target_path = target_folder / f\"{self.pdf_name}_{self.target_language}.pdf\"\n        print('save path ',target_path)\n        self.doc.ez_save(\n            target_path,\n            garbage=4,\n            deflate=True\n        )\n\n\n\n        # 8. 打印耗时\n        end_time = time.time()\n        print(end_time - self.t)\n\n    def start(self, image, pag_num):\n        \"\"\"\n        原先逐页处理的函数，现仅负责“提取文本并存储在 self.pages_data[pag_num]”。\n        不在这里直接翻译或写回 PDF。\n        \"\"\"\n        # 确保 self.pages_data 有 pag_num 对应的列表\n        while len(self.pages_data) <= pag_num:\n            self.pages_data.append([])  # 每个元素是 [ [text, (x0,y0,x1,y1)], ... ]\n\n        page = self.doc.load_page(pag_num)\n\n        if self.line_model and self.use_mupdf:\n            def snap_angle_func(angle):\n                \"\"\"\n                将任意角度自动映射到 0、90、180、270 四个值之一。\n                \"\"\"\n                # 将角度映射到 [0, 360) 区间\n                angle = abs(angle) % 360\n                # 选取最接近的标准角度\n                possible_angles = [0, 90, 180, 270]\n                return min(possible_angles, key=lambda x: abs(x - angle))\n\n            blocks = page.get_text(\"dict\")[\"blocks\"]\n            for block in blocks:\n                if block.get(\"type\") == 0:  # 文本块\n                    font_info = None\n                    # 遍历每一行\n                    for line in block[\"lines\"]:\n                        # 1) 拼接文本（减少反复 += 操作）\n                        span_texts = [span[\"text\"] for span in line[\"spans\"] if \"text\" in span]\n                        line_text = \"\".join(span_texts).strip()\n\n                        # 2) 如果行文本为空或仅含数字标点，就跳过\n                        if not line_text or line_non_text(line_text):\n                            continue\n\n                        # 3) 此时才计算旋转角度，避免空行浪费\n                        direction = line.get(\"dir\", [1.0, 0.0])\n                        raw_angle = math.degrees(math.atan2(direction[1], direction[0]))\n                        angle = snap_angle_func(raw_angle)\n\n                        # 4) 只在需要时提取字体信息\n                        if not font_info:\n                            for span in line[\"spans\"]:\n                                if \"font\" in span:\n                                    font_info = span[\"font\"]\n                                    break\n                        if font_info and font_info not in font_collection:\n                            font_collection.append(font_info)\n\n                        line_bbox = line.get(\"bbox\")\n                        # 5) 加入提取结果\n                        self.pages_data[pag_num].append([\n                            line_text,  # 原文\n                            tuple(line_bbox),  # BBox\n                            None,  # 预留翻译文本\n                            angle  # 行角度\n                        ])\n\n\n        # 如果用 PyMuPDF 提取文字\n        elif self.use_mupdf and image is None:\n            blocks = page.get_text(\"dict\")[\"blocks\"]\n            for block in blocks:\n                if block.get(\"type\") == 0:  # 文本块\n                    bbox = block[\"bbox\"]\n                    text = \"\"\n                    font_info = None\n                    for line in block[\"lines\"]:\n                        for span in line[\"spans\"]:\n                            span_text = span[\"text\"].strip()\n                            if span_text:\n                                text += span_text + \" \"\n                                if not font_info and \"font\" in span:\n                                    font_info = span[\"font\"]\n                    text = text.strip()\n                    if text and not is_math(text, pag_num, font_info) and not is_non_text(text):\n                        self.pages_data[pag_num].append([text, tuple(bbox),None])\n\n        else:\n            # OCR 提取文字\n            config = self.config\n            tesseract_path = config['ocr_services']['tesseract']['path']\n            pytesseract.pytesseract.tesseract_cmd = tesseract_path\n\n            Full_width, Full_height = image.size\n            ocr_result = pytesseract.image_to_data(image, output_type=pytesseract.Output.DICT)\n\n            current_paragraph_text = ''\n            paragraph_bbox = {\n                'left': float('inf'),\n                'top': float('inf'),\n                'right': 0,\n                'bottom': 0\n            }\n            current_block_num = None\n            Threshold_width = 0.06 * Full_width\n            Threshold_height = 0.006 * Full_height\n\n            for i in range(len(ocr_result['text'])):\n                block_num = ocr_result['block_num'][i]\n                text_ocr = ocr_result['text'][i].strip()\n                left = ocr_result['left'][i]\n                top = ocr_result['top'][i]\n                width = ocr_result['width'][i]\n                height = ocr_result['height'][i]\n\n                if text_ocr and not is_math(text_ocr, pag_num, font_info='22') and not is_non_text(text_ocr):\n\n                    # 若换 block 或段落间隔较大，则保存上一段\n                    if (block_num != current_block_num or\n                       (abs(left - paragraph_bbox['right']) > Threshold_width and\n                        abs(height - (paragraph_bbox['bottom'] - paragraph_bbox['top'])) > Threshold_height and\n                        abs(left - paragraph_bbox['left']) > Threshold_width)):\n\n                        if current_paragraph_text:\n                            # 转换到 PDF 坐标\n                            Full_rect = page.rect\n                            w_points = Full_rect.width\n                            h_points = Full_rect.height\n\n                            x0_ratio = paragraph_bbox['left'] / Full_width\n                            y0_ratio = paragraph_bbox['top'] / Full_height\n                            x1_ratio = paragraph_bbox['right'] / Full_width\n                            y1_ratio = paragraph_bbox['bottom'] / Full_height\n\n                            x0_pdf = x0_ratio * w_points\n                            y0_pdf = y0_ratio * h_points\n                            x1_pdf = x1_ratio * w_points\n                            y1_pdf = y1_ratio * h_points\n\n                            self.pages_data[pag_num].append([\n                                current_paragraph_text.strip(),\n                                (x0_pdf, y0_pdf, x1_pdf, y1_pdf)\n                            ])\n\n                        # 重置\n                        current_paragraph_text = ''\n                        paragraph_bbox = {\n                            'left': float('inf'),\n                            'top': float('inf'),\n                            'right': 0,\n                            'bottom': 0\n                        }\n                        current_block_num = block_num\n\n                    # 继续累加文本\n                    current_paragraph_text += text_ocr + \" \"\n                    paragraph_bbox['left'] = min(paragraph_bbox['left'], left)\n                    paragraph_bbox['top'] = min(paragraph_bbox['top'], top)\n                    paragraph_bbox['right'] = max(paragraph_bbox['right'], left + width)\n                    paragraph_bbox['bottom'] = max(paragraph_bbox['bottom'], top + height)\n\n            # 收尾：最后一段存入\n            if current_paragraph_text:\n                Full_rect = page.rect\n                w_points = Full_rect.width\n                h_points = Full_rect.height\n\n                x0_ratio = paragraph_bbox['left'] / Full_width\n                y0_ratio = paragraph_bbox['top'] / Full_height\n                x1_ratio = paragraph_bbox['right'] / Full_width\n                y1_ratio = paragraph_bbox['bottom'] / Full_height\n\n                x0_pdf = x0_ratio * w_points\n                y0_pdf = y0_ratio * h_points\n                x1_pdf = x1_ratio * w_points\n                y1_pdf = y1_ratio * h_points\n\n                self.pages_data[pag_num].append([\n                    current_paragraph_text.strip(),\n                    (x0_pdf, y0_pdf, x1_pdf, y1_pdf),\n                    None\n                ])\n\n        # 注意：这里不做翻译、不插入 PDF，只负责“收集文本”到 self.pages_data\n\n    def batch_translate_pages_data(self, original_language, target_language,\n                                   translation_type, batch_size=None ):\n        \"\"\"PPC (Pages Per Call)\n        分批翻译 pages_data，每次处理最多 batch_size 页的文本，避免一次性过多。\n        将译文存回 self.pages_data 的第三个元素，如 [原文, bbox, 译文]\n        \"\"\"\n        total_pages = len(self.pages_data)\n        start_idx = 0\n\n        while start_idx < total_pages:\n            end_idx = min(start_idx + batch_size, total_pages)\n\n            # 收集该批次的所有文本\n            batch_texts = []\n            for i in range(start_idx, end_idx):\n                for block in self.pages_data[i]:\n                    batch_texts.append(block[0])  # block[0] = 原文\n\n            # 翻译\n\n\n            if self.translation and self.use_mupdf:\n                translation_list = at.Online_translation(\n                    original_language=original_language,\n                    target_language=target_language,\n                    translation_type=translation_type,\n                    texts_to_process=batch_texts\n                ).translation()\n            elif self.translation and not self.use_mupdf:\n                # 离线翻译\n                translation_list = at.Offline_translation(\n                    original_language=original_language,\n                    target_language=target_language,\n                    texts_to_process=batch_texts\n                ).translation()\n            else:\n\n                translation_list = batch_texts\n\n\n\n            # 回填译文\n            idx_t = 0\n            for i in range(start_idx, end_idx):\n                for block in self.pages_data[i]:\n                    # 在第三个位置添加翻译文本\n                    block[2] = translation_list[idx_t]\n                    idx_t += 1\n\n            start_idx += batch_size\n\n    def apply_translations_to_pdf(self):\n        \"\"\"\n        统一对 PDF 做“打码/打白 + 插入译文”操作\n        \"\"\"\n        for page_index, blocks in enumerate(self.pages_data):\n            page = self.doc.load_page(page_index)\n\n            for block in blocks:\n                original_text = block[0]\n                coords = block[1]  # (x0, y0, x1, y1)\n                # 如果第三个元素是译文，则用之，否则用原文\n                translated_text = block[2] if len(block) >= 3 else original_text\n\n                if self.line_model:\n                    angle = block[3] if len(block) > 3 else 0\n\n                else:\n                    angle = 0\n\n                rect = fitz.Rect(*coords)\n\n                # 先尝试使用 Redact 遮盖\n                try:\n                    page.add_redact_annot(rect)\n                    page.apply_redactions()\n                except Exception as e:\n                    # 若 Redact 失败，改用白色方块覆盖\n                    annots = list(page.annots() or [])\n                    if annots:\n                        page.delete_annot(annots[-1])\n                    try:\n                        page.draw_rect(rect, color=(1, 1, 1), fill=(1, 1, 1))\n                    except Exception as e2:\n                        print(f\"创建白色画布时发生错误: {e2}\")\n                    print(f\"应用重编辑时发生错误: {e}\")\n\n\n\n                page.insert_htmlbox(\n                    rect,\n                    translated_text,\n                    css=\"\"\"\n                * {\n                    font-family: \"Microsoft YaHei\";\n                    /* 这行可把内容改成粗体, 可写 \"bold\" 或数字 (100–900) */\n                    font-weight: 100;\n                    /* 这里可以使用标准 CSS 颜色写法, 例如 #FF0000、rgb() 等 */\n                    color:  #333333;\n                }\n                \"\"\",\n                    rotate=angle\n\n                )\n\n\nif __name__ == '__main__':\n\n    main_function(original_language='auto', target_language='zh', pdf_path='g2.pdf',en=30,bn=0).main()"
  },
  {
    "path": "EbookTranslator/LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "EbookTranslator/README.md",
    "content": "English | [简体中文](https://github.com/CBIhalsen/PolyglotPDF/blob/main//README_CN.md) | [繁體中文](https://github.com/CBIhalsen/PolyglotPDF/blob/main/README_TW.md) | [日本語](https://github.com/CBIhalsen/PolyglotPDF/blob/main/README_JA.md) | [한국어](https://github.com/CBIhalsen/PolyglotPDF/blob/main/README_KO.md)\n# PolyglotPDF\n\n[![Python](https://img.shields.io/badge/python-3.8-blue.svg)](https://www.python.org/)\n[![PDF](https://img.shields.io/badge/pdf-documentation-brightgreen.svg)](https://example.com)\n[![LaTeX](https://img.shields.io/badge/latex-typesetting-orange.svg)](https://www.latex-project.org/)\n[![Translation](https://img.shields.io/badge/translation-supported-yellow.svg)](https://example.com)\n[![Math](https://img.shields.io/badge/math-formulas-red.svg)](https://example.com)\n[![PyMuPDF](https://img.shields.io/badge/PyMuPDF-1.24.0-blue.svg)](https://pymupdf.readthedocs.io/)\n\n\n## Demo\n<img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/demo.gif?raw=true\" width=\"80%\" height=\"40%\">\n\n## Speed comparison\n<img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/Figure_1.png?raw=true\" width=\"80%\" height=\"40%\">\n\n### [🎬 Watch Full Video](https://github.com/CBIhalsen/PolyglotPDF/blob/main/demo.mp4)\n llms has been added as the translation api of choice, Doubao ,Qwen ,deepseek v3 , gpt4-o-mini are recommended. The color space error can be resolved by filling the white areas in PDF files. The old text to text translation api has been removed.\n\nIn addition, consider adding arxiv search function and rendering arxiv papers after latex translation.\n\n### Pasges show\n<div style=\"display: flex; margin-bottom: 20px;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page1.png?raw=true\" width=\"40%\" height=\"20%\" style=\"margin-right: 20px;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page2.jpeg?raw=true\" width=\"40%\" height=\"20%\">\n</div>\n<div style=\"display: flex;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page3.png?raw=true\" width=\"40%\" height=\"20%\" style=\"margin-right: 20px;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page4.png?raw=true\" width=\"40%\" height=\"20%\">\n</div>\n\n\n# Chinese LLM API Application\n\n## Doubao & Deepseek\nApply through Volcengine platform:\n- Application URL: [Volcengine-Doubao](https://www.volcengine.com/product/doubao/)\n- Available Models: Doubao, Deepseek series models\n\n## Tongyi Qwen\nApply through Alibaba Cloud platform:\n- Application URL: [Alibaba Cloud-Tongyi Qwen](https://cn.aliyun.com/product/tongyi?from_alibabacloud=&utm_content=se_1019997984)\n- Available Models: Qwen-Max, Qwen-Plus series models\n\n\n## Overview\nPolyglotPDF(EbookTranslation) is an advanced PDF processing tool that employs specialized techniques for ultra-fast text, table, and formula recognition in PDF documents, typically completing processing within 1 second. It features OCR capabilities and layout-preserving translation, with full document translations usually completed within 10 seconds (speed may vary depending on the translation API provider).\n\n## Features\n- **Ultra-Fast Recognition**: Processes text, tables, and formulas in PDFs within ~1 second\n- **Layout-Preserving Translation**: Maintains original document formatting while translating content\n- **OCR Support**: Handles scanned documents efficiently\n- **Text-based PDF**：No GPU required\n- **Quick Translation**: Complete PDF translation in approximately 10 seconds\n- **Flexible API Integration**: Compatible with various translation service providers\n- **Web-based Comparison Interface**: Side-by-side comparison of original and translated documents\n- **Enhanced OCR Capabilities**: Improved accuracy in text recognition and processing\n- **Support for offline translation**: Use smaller translation model\n\n## Installation and Setup\n\n\n\n### There are several ways to use it. One is to install the library,\n\n```bash\npip install EbookTranslator\n```\n\n\n\nBasic usage:\n\n```bash\nEbookTranslator your_file.pdf\n```\n\nUsage with parameters:\n\n```bash\nEbookTranslator your_file.pdf -o en -t zh -b 1 -e 10 -c /path/to/config.json -d 300\n```\n\n####  Using in Python Code\n\n```python\nfrom EbookTranslator import main_function\n\ntranslator = main_function(\n    pdf_path=\"your_file.pdf\",\n    original_language=\"en\",\n    target_language=\"zh\",\n    bn=1,\n    en=10,\n    config_path=\"/path/to/config.json\",\n    DPI=300\n)\ntranslator.main()\n```\n\n## Parameter Description\n\n| Parameter | Command Line Option | Description | Default Value |\n|-----------|---------------------|-------------|---------------|\n| `pdf_path` | Positional argument | PDF file path | Required |\n| `original_language` | `-o, --original` | Source language | `auto` |\n| `target_language` | `-t, --target` | Target language | `zh` |\n| `bn` | `-b, --begin` | Starting page number | `1` |\n| `en` | `-e, --end` | Ending page number | Last page of the document |\n| `config_path` | `-c, --config` | Configuration file path | `config.json` in the current working directory |\n| `DPI` | `-d, --dpi` | DPI for OCR mode | `72` |\n\n#### Configuration File\n\nThe configuration file is a JSON file, by default located at `config.json` in the current working directory. If it doesn't exist, the program will use built-in default settings.\n\n#### Configuration File Example\n\n```json\n{\n  \"count\": 4,\n  \"PPC\": 20,\n  \"translation_services\": {\n    \"Doubao\": {\n      \"auth_key\": \"\",\n      \"model_name\": \"\"\n    },\n    \"Qwen\": {\n      \"auth_key\": \"\",\n      \"model_name\": \"qwen-plus\"\n    },\n    \"deepl\": {\n      \"auth_key\": \"\"\n    },\n    \"deepseek\": {\n      \"auth_key\": \"\",\n      \"model_name\": \"ep-20250218224909-gps4n\"\n    },\n    \"openai\": {\n      \"auth_key\": \"\",\n      \"model_name\": \"gpt-4o-mini\"\n    },\n    \"youdao\": {\n      \"app_key\": \"\",\n      \"app_secret\": \"\"\n    }\n  },\n  \"ocr_services\": {\n    \"tesseract\": {\n      \"path\": \"C:\\\\Program Files\\\\Tesseract-OCR\\\\tesseract.exe\"\n    }\n  },\n  \"default_services\": {\n    \"ocr_model\": false,\n    \"line_model\": false,\n    \"Enable_translation\": true,\n    \"Translation_api\": \"openai\"\n  }\n}\n```\n\n#### Configuration Options\n\n- `translation_service`: Translation service provider (e.g., \"google\", \"deepl\", \"baidu\")\n- `api_key`: Translation API key (if required)\n- `translation_mode`: Translation mode, \"online\" or \"offline\"\n- `ocr_enabled`: Whether to enable OCR recognition\n- `tesseract_path`: Path to Tesseract OCR engine (if not in system PATH)\n- `output_dir`: Output directory\n- `language_codes`: Language code mapping\n- `font_mapping`: Fonts corresponding to different languages\n\n\n#### Output\n\nTranslated PDF files will be saved in the directory specified by `output_dir` (default is the `target` folder in the current working directory).\n\n\n\n\n## License\n\nMIT\n\n## Use method for friendly UI interface\n\n1. Clone the repository:\n```bash\ngit clone https://github.com/CBIhalsen/PolyglotPDF.git\ncd polyglotpdf\n```\n\n2. Install required packages:\n```bash\npip install -r requirements.txt\n```\n3. Configure your API key in config.json. The alicloud translation API is not recommended.\n\n4. Run the application:\n```bash\npython app.py\n```\n\n5. Access the web interface:\nOpen your browser and navigate to `http://127.0.0.1:8000`\n\n## Requirements\n- Python 3.8+\n- deepl==1.17.0\n- Flask==2.0.1\n- Flask-Cors==5.0.0\n- langdetect==1.0.9\n- Pillow==10.2.0\n- PyMuPDF==1.24.0\n- pytesseract==0.3.10\n- requests==2.31.0\n- tiktoken==0.6.0\n- Werkzeug==2.0.1\n\n## Acknowledgments\nThis project leverages PyMuPDF's capabilities for efficient PDF processing and layout preservation.\n\n## Upcoming Improvements\n- PDF chat functionality\n- Academic PDF search integration\n- Optimization for even faster processing speeds\n\n### Known Issues\n- **Issue Description**: Error during text re-editing: `code=4: only Gray, RGB, and CMYK colorspaces supported`\n- **Symptom**: Unsupported color space encountered during text block editing\n- **Current Workaround**: Skip text blocks with unsupported color spaces\n- **Proposed Solution**: Switch to OCR mode for entire pages containing unsupported color spaces\n- **Example**: [View PDF sample with unsupported color spaces](https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/colorspace_issue_sample.pdf)\n\n\n### Font Optimization\nCurrent font configuration in the `start` function of `main.py`:\n```python\n# Current configuration\ncss=f\"* {{font-family:{get_font_by_language(self.target_language)};font-size:auto;color: #111111 ;font-weight:normal;}}\"\n```\n\nYou can optimize font display through the following methods:\n\n1. **Modify Default Font Configuration**\n```python\n# Custom font styles\ncss=f\"\"\"* {{\n    font-family: {get_font_by_language(self.target_language)};\n    font-size: auto;\n    color: #111111;\n    font-weight: normal;\n    letter-spacing: 0.5px;  # Adjust letter spacing\n    line-height: 1.5;      # Adjust line height\n}}\"\"\"\n```\n\n2. **Embed Custom Fonts**\nYou can embed custom fonts by following these steps:\n- Place font files (.ttf, .otf) in the project's `fonts` directory\n- Use `@font-face` to declare custom fonts in CSS\n```python\ncss=f\"\"\"\n@font-face {{\n    font-family: 'CustomFont';\n    src: url('fonts/your-font.ttf') format('truetype');\n}}\n* {{\n    font-family: 'CustomFont', {get_font_by_language(self.target_language)};\n    font-size: auto;\n    font-weight: normal;\n}}\n\"\"\"\n```\n\n### Basic Principles\nThis project follows similar basic principles as Adobe Acrobat DC's PDF editing, using PyMuPDF for text block recognition and manipulation:\n\n- **Core Process**:\n```python\n# Get text blocks from the page\nblocks = page.get_text(\"dict\")[\"blocks\"]\n\n# Process each text block\nfor block in blocks:\n    if block.get(\"type\") == 0:  # text block\n        bbox = block[\"bbox\"]     # get text block boundary\n        text = \"\"\n        font_info = None\n        # Collect text and font information\n        for line in block[\"lines\"]:\n            for span in line[\"spans\"]:\n                text += span[\"text\"] + \" \"\n```\nThis approach directly processes PDF text blocks, maintaining the original layout while achieving efficient text extraction and modification.\n\n- **Technical Choices**:\n  - Utilizes PyMuPDF for PDF parsing and editing\n  - Focuses on text processing\n  - Avoids complex operations like AI formula recognition, table processing, or page restructuring\n\n- **Why Avoid Complex Processing**:\n  - AI recognition of formulas, tables, and PDF restructuring faces severe performance bottlenecks\n  - Complex AI processing leads to high computational costs\n  - Significantly increased processing time (potentially tens of seconds or more)\n  - Difficult to deploy at scale with low costs in production environments\n  - Not suitable for online services requiring quick response times\n\n- **Project Scope**:\n  - This project only serves to demonstrate the correct approach for layout-preserved PDF translation and AI-assisted PDF reading. Converting PDF files to markdown format for large language models to read, in my opinion, is not a wise approach.\n  - Aims for optimal performance-to-cost ratio\n\n- **Performance**:\n  - PolyglotPDF API response time: ~1 second per page\n  - Low computational resource requirements, suitable for scale deployment\n  - High cost-effectiveness for commercial applications\n\n- * Contact author:\nQQ： 1421243966\nemail: 1421243966@qq.com\n\nRelated questions answered and discussed：\n\n QQ group:\n 1031477425\n\n\n\n"
  },
  {
    "path": "EbookTranslator/requirements.txt",
    "content": "deepl==1.17.0\nFlask\nflask-cors\nPillow==10.2.0\nPyMuPDF==1.24.0\npytesseract==0.3.10\nrequests==2.31.0\nWerkzeug==2.0.1\naiohttp\n"
  },
  {
    "path": "EbookTranslator/setup.py",
    "content": "from setuptools import setup, find_packages\n\nwith open(\"README.md\", \"r\", encoding=\"utf-8\") as fh:\n    long_description = fh.read()\n\nsetup(\n    name=\"EbookTranslator\",\n    version=\"0.3.3\",\n    author=\"Chen\",\n    author_email=\"1421243966@qq.com\",\n    description=\"The world's highest performing e-book retention layout translation library\",\n    long_description=long_description,  # 添加这一行\n    long_description_content_type=\"text/markdown\",\n    url=\"https://github.com/1421243966/EbookTranslator\",  # 更新为您的实际GitHub仓库\n    packages=find_packages(),\n    classifiers=[\n        \"Programming Language :: Python :: 3\",\n        \"Programming Language :: Python :: 3.6\",\n        \"Programming Language :: Python :: 3.7\",\n        \"Programming Language :: Python :: 3.8\",\n        \"Programming Language :: Python :: 3.9\",\n        \"Programming Language :: Python :: 3.10\",\n        \"License :: OSI Approved :: MIT License\",\n        \"Operating System :: OS Independent\",\n        \"Development Status :: 4 - Beta\",\n        \"Intended Audience :: Developers\",\n        \"Intended Audience :: Education\",\n        \"Intended Audience :: Science/Research\",\n        \"Topic :: Text Processing :: Linguistic\",\n        \"Topic :: Utilities\",\n    ],\n    python_requires=\">=3.6\",\n    install_requires=[\n        \"pymupdf>=1.18.0\",\n        \"Pillow>=8.0.0\",\n        \"pytesseract>=0.3.0\",\n        \"deepl>=1.17.0\",\n        \"requests>=2.25.0\",\n        \"Werkzeug>=2.0.0\",\n        \"aiohttp>=3.7.4\",\n    ],\n    entry_points={\n        \"console_scripts\": [\n            \"EbookTranslator=EbookTranslator.cli:main\",\n        ],\n    },\n    include_package_data=True,\n    keywords=[\"ebook\", \"translation\", \"pdf\", \"ocr\", \"nlp\", \"language\"],\n    project_urls={\n        \"Bug Reports\": \"https://github.com/1421243966/EbookTranslator/issues\",\n        \"Source\": \"https://github.com/1421243966/EbookTranslator\",\n        \"Documentation\": \"https://github.com/1421243966/EbookTranslator#readme\",\n    },\n)\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "LLMS_translation.py",
    "content": "import asyncio\r\nimport aiohttp\r\nimport re\r\nimport requests\r\n\r\nimport load_config\r\n\r\nclass AI302_translation:\r\n    def __init__(self):\r\n        config = load_config.load_config()\r\n        self.api_key = config['translation_services']['AI302']['auth_key']\r\n        self.url = \"https://api.302.ai/v1/chat/completions\"\r\n        self.headers = {\r\n            \"Authorization\": f\"Bearer {self.api_key}\",\r\n            \"Content-Type\": \"application/json\"\r\n        }\r\n        self.model = config['translation_services']['AI302']['model_name']\r\n        # 从配置中读取翻译提示词\r\n        self.prompt_template = config.get('translation_prompt', {}).get('system_prompt', \r\n            'You are a professional translator. Translate from {original_lang} to {target_lang}. Return only the translation without explanations or notes.')\r\n\r\n    async def translate_single(self, session, text, original_lang, target_lang):\r\n        \"\"\"单个文本的异步翻译\"\"\"\r\n        payload = {\r\n            \"model\": self.model,\r\n            \"messages\": [\r\n                {\r\n                    \"role\": \"system\",\r\n                    \"content\": self.prompt_template.format(original_lang=original_lang, target_lang=target_lang)\r\n                },\r\n                {\r\n                    \"role\": \"user\",\r\n                    \"content\": text\r\n                }\r\n            ],\r\n            \"response_format\": {\r\n                \"type\": \"text\"\r\n            },\r\n            \"temperature\": 0.3,\r\n            \"top_p\": 0.9\r\n        }\r\n\r\n        try:\r\n            async with session.post(self.url, headers=self.headers, json=payload) as response:\r\n                if response.status == 200:\r\n                    result = await response.json()\r\n                    return result['choices'][0]['message']['content'].strip()\r\n                else:\r\n                    print(f\"Error: {response.status}\")\r\n                    return \"\"\r\n        except Exception as e:\r\n            print(f\"Error in translation: {e}\")\r\n            return \"\"\r\n\r\n    async def translate(self, texts, original_lang, target_lang):\r\n        \"\"\"异步批量翻译\"\"\"\r\n        print(f\"Starting 302.ai translation of {len(texts)} texts\")\r\n        async with aiohttp.ClientSession() as session:\r\n            tasks = [\r\n                self.translate_single(session, text, original_lang, target_lang)\r\n                for text in texts\r\n            ]\r\n            results = await asyncio.gather(*tasks)\r\n            print(f\"302.ai translation completed, {len(results)} texts translated\")\r\n            return results\r\n\r\nclass Openai_translation:\r\n    def __init__(self):\r\n        config = load_config.load_config()\r\n        self.api_key = config['translation_services']['openai']['auth_key']\r\n        self.url = \"https://api.openai.com/v1/chat/completions\"\r\n        self.headers = {\r\n            \"Authorization\": f\"Bearer {self.api_key}\",\r\n            \"Content-Type\": \"application/json\"\r\n        }\r\n        self.model = config['translation_services']['openai']['model_name']\r\n        # 从配置中读取翻译提示词\r\n        self.prompt_template = config.get('translation_prompt', {}).get('system_prompt', \r\n            'You are a professional translator. Translate from {original_lang} to {target_lang}. Return only the translation without explanations or notes.')\r\n\r\n    async def translate_single(self, session, text, original_lang, target_lang):\r\n        \"\"\"单个文本的异步翻译\"\"\"\r\n        payload = {\r\n            \"model\": self.model,\r\n            \"messages\": [\r\n                {\r\n                    \"role\": \"system\",\r\n                    \"content\": self.prompt_template.format(original_lang=original_lang, target_lang=target_lang)\r\n                },\r\n                {\r\n                    \"role\": \"user\",\r\n                    \"content\": text\r\n                }\r\n            ],\r\n            \"response_format\": {\r\n                \"type\": \"text\"\r\n            },\r\n            \"temperature\": 0.3,\r\n            \"top_p\": 0.9\r\n        }\r\n\r\n        try:\r\n            async with session.post(self.url, headers=self.headers, json=payload) as response:\r\n                if response.status == 200:\r\n                    result = await response.json()\r\n                    return result['choices'][0]['message']['content'].strip()\r\n                else:\r\n                    print(f\"Error: {response.status}\")\r\n                    return \"\"\r\n        except Exception as e:\r\n            print(f\"Error in translation: {e}\")\r\n            return \"\"\r\n\r\n    async def translate(self, texts, original_lang, target_lang):\r\n        \"\"\"异步批量翻译\"\"\"\r\n        async with aiohttp.ClientSession() as session:\r\n            tasks = [\r\n                self.translate_single(session, text, original_lang, target_lang)\r\n                for text in texts\r\n            ]\r\n            return await asyncio.gather(*tasks)\r\n\r\n# 添加Bing翻译类\r\nclass Bing_translation:\r\n    def __init__(self):\r\n        self.session = requests.Session()\r\n        self.endpoint = \"https://www.bing.com/translator\"\r\n        self.headers = {\r\n            \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0\",\r\n        }\r\n        self.lang_map = {\"zh\": \"zh-Hans\"}\r\n\r\n    async def find_sid(self):\r\n        \"\"\"获取必要的会话参数\"\"\"\r\n        loop = asyncio.get_event_loop()\r\n        response = await loop.run_in_executor(None, lambda: self.session.get(self.endpoint, headers=self.headers))\r\n        response.raise_for_status()\r\n        url = response.url[:-10]\r\n        ig = re.findall(r\"\\\"ig\\\":\\\"(.*?)\\\"\", response.text)[0]\r\n        iid = re.findall(r\"data-iid=\\\"(.*?)\\\"\", response.text)[-1]\r\n        key, token = re.findall(\r\n            r\"params_AbusePreventionHelper\\s=\\s\\[(.*?),\\\"(.*?)\\\",\", response.text\r\n        )[0]\r\n        return url, ig, iid, key, token\r\n\r\n    async def translate_single(self, session, text, original_lang, target_lang):\r\n        \"\"\"单个文本的异步翻译\"\"\"\r\n        if not text or not text.strip():\r\n            return \"\"\r\n            \r\n        # 处理语言代码映射\r\n        lang_in = self.lang_map.get(original_lang, original_lang)\r\n        lang_out = self.lang_map.get(target_lang, target_lang)\r\n        \r\n        # 自动语言检测处理\r\n        if lang_in == \"auto\":\r\n            lang_in = \"auto-detect\"\r\n        \r\n        # Bing翻译最大长度限制\r\n        text = text[:1000]\r\n        \r\n        try:\r\n            url, ig, iid, key, token = await self.find_sid()\r\n            \r\n            # 通过异步HTTP请求执行翻译\r\n            async with session.post(\r\n                f\"{url}ttranslatev3?IG={ig}&IID={iid}\",\r\n                data={\r\n                    \"fromLang\": lang_in,\r\n                    \"to\": lang_out,\r\n                    \"text\": text,\r\n                    \"token\": token,\r\n                    \"key\": key,\r\n                },\r\n                headers=self.headers,\r\n            ) as response:\r\n                if response.status == 200:\r\n                    result = await response.json()\r\n                    return result[0][\"translations\"][0][\"text\"]\r\n                else:\r\n                    error_text = await response.text()\r\n                    print(f\"Bing翻译错误: {response.status}, 详情: {error_text}\")\r\n                    return \"\"\r\n        except Exception as e:\r\n            print(f\"Bing翻译过程中发生错误: {e}\")\r\n            return \"\"\r\n\r\n    async def translate(self, texts, original_lang, target_lang):\r\n        \"\"\"异步批量翻译\"\"\"\r\n        print(f\"开始Bing翻译，共 {len(texts)} 个文本\")\r\n        async with aiohttp.ClientSession() as session:\r\n            tasks = []\r\n            for i, text in enumerate(texts):\r\n                # 添加延迟以避免请求过快被屏蔽\r\n                await asyncio.sleep(0.5 * (i % 3))  # 每3个请求一组，每组之间间隔0.5秒\r\n                tasks.append(self.translate_single(session, text, original_lang, target_lang))\r\n            \r\n            results = await asyncio.gather(*tasks)\r\n            print(f\"Bing翻译完成，共翻译 {len(results)} 个文本\")\r\n            return results\r\n\r\nclass Deepseek_translation:\r\n    def __init__(self):\r\n        config = load_config.load_config()\r\n        self.api_key = config['translation_services']['deepseek']['auth_key']\r\n        self.url = \"https://ark.cn-beijing.volces.com/api/v3/chat/completions\"\r\n        self.headers = {\r\n            \"Authorization\": f\"Bearer {self.api_key}\",\r\n            \"Content-Type\": \"application/json\"\r\n        }\r\n        self.model = config['translation_services']['deepseek']['model_name']\r\n        # 从配置中读取翻译提示词\r\n        self.prompt_template = config.get('translation_prompt', {}).get('system_prompt', \r\n            'You are a professional translator. Translate from {original_lang} to {target_lang}. Return only the translation without explanations or notes.')\r\n\r\n    async def translate_single(self, session, text, original_lang, target_lang):\r\n        \"\"\"单个文本的异步翻译\"\"\"\r\n        payload = {\r\n            \"model\": self.model,\r\n            \"messages\": [\r\n                {\r\n                    \"role\": \"system\",\r\n                    \"content\": self.prompt_template.format(original_lang=original_lang, target_lang=target_lang)\r\n                },\r\n                {\r\n                    \"role\": \"user\",\r\n                    \"content\": text\r\n                }\r\n            ],\r\n            \"temperature\": 0.3,\r\n            \"top_p\": 0.9\r\n        }\r\n\r\n        try:\r\n            async with session.post(self.url, headers=self.headers, json=payload) as response:\r\n                if response.status == 200:\r\n                    result = await response.json()\r\n                    return result['choices'][0]['message']['content'].strip()\r\n                else:\r\n                    print(f\"Error: {response.status}\")\r\n                    return \"\"\r\n        except Exception as e:\r\n            print(f\"Error in translation: {e}\")\r\n            return \"\"\r\n\r\n    async def translate(self, texts, original_lang, target_lang):\r\n        \"\"\"异步批量翻译\"\"\"\r\n        async with aiohttp.ClientSession() as session:\r\n            tasks = [\r\n                self.translate_single(session, text, original_lang, target_lang)\r\n                for text in texts\r\n            ]\r\n            return await asyncio.gather(*tasks)\r\nclass Doubao_translation:\r\n    def __init__(self):\r\n        config = load_config.load_config()\r\n        self.api_key = config['translation_services']['Doubao']['auth_key']\r\n        self.url = \"https://ark.cn-beijing.volces.com/api/v3/chat/completions\"\r\n        self.headers = {\r\n            \"Authorization\": f\"Bearer {self.api_key}\",\r\n            \"Content-Type\": \"application/json\"\r\n        }\r\n        self.model = config['translation_services']['Doubao']['model_name']\r\n        # 从配置中读取翻译提示词\r\n        self.prompt_template = config.get('translation_prompt', {}).get('system_prompt', \r\n            'You are a professional translator. Translate from {original_lang} to {target_lang}. Return only the translation without explanations or notes.')\r\n\r\n    async def translate_single(self, session, text, original_lang, target_lang):\r\n        \"\"\"单个文本的异步翻译\"\"\"\r\n        payload = {\r\n            \"model\": self.model,\r\n            \"messages\": [\r\n                {\r\n                    \"role\": \"system\",\r\n                    \"content\": self.prompt_template.format(original_lang=original_lang, target_lang=target_lang)\r\n                },\r\n                {\r\n                    \"role\": \"user\",\r\n                    \"content\": text\r\n                }\r\n            ],\r\n            \"temperature\": 0.3,\r\n            \"top_p\": 0.9\r\n        }\r\n\r\n        try:\r\n            async with session.post(self.url, headers=self.headers, json=payload) as response:\r\n                if response.status == 200:\r\n                    result = await response.json()\r\n                    return result['choices'][0]['message']['content'].strip()\r\n                else:\r\n                    print(f\"Error: {response.status}\")\r\n                    return \"\"\r\n        except Exception as e:\r\n            print(f\"Error in translation: {e}\")\r\n            return \"\"\r\n\r\n    async def translate(self, texts, original_lang, target_lang):\r\n        \"\"\"异步批量翻译\"\"\"\r\n        async with aiohttp.ClientSession() as session:\r\n            tasks = [\r\n                self.translate_single(session, text, original_lang, target_lang)\r\n                for text in texts\r\n            ]\r\n            return await asyncio.gather(*tasks)\r\nclass Qwen_translation:\r\n    def __init__(self):\r\n        config = load_config.load_config()\r\n        self.api_key = config['translation_services']['Qwen']['auth_key']\r\n        self.url = \"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions\"\r\n        self.headers = {\r\n            \"Authorization\": f\"Bearer {self.api_key}\",\r\n            \"Content-Type\": \"application/json\"\r\n        }\r\n        self.model = config['translation_services']['Qwen']['model_name']\r\n        # 从配置中读取翻译提示词\r\n        self.prompt_template = config.get('translation_prompt', {}).get('system_prompt', \r\n            'You are a professional translator. Translate from {original_lang} to {target_lang}. Return only the translation without explanations or notes.')\r\n\r\n    async def translate_single(self, session, text, original_lang, target_lang):\r\n        \"\"\"单个文本的异步翻译\"\"\"\r\n        payload = {\r\n            \"model\": self.model,\r\n            \"messages\": [\r\n                {\r\n                    \"role\": \"system\",\r\n                    \"content\": self.prompt_template.format(original_lang=original_lang, target_lang=target_lang)\r\n                },\r\n                {\r\n                    \"role\": \"user\",\r\n                    \"content\": text\r\n                }\r\n            ],\r\n            \"temperature\": 0.3,\r\n            \"top_p\": 0.9\r\n        }\r\n\r\n        try:\r\n            async with session.post(self.url, headers=self.headers, json=payload) as response:\r\n                if response.status == 200:\r\n                    result = await response.json()\r\n                    return result['choices'][0]['message']['content'].strip()\r\n                else:\r\n                    print(f\"Error: {response.status}\")\r\n                    return \"\"\r\n        except Exception as e:\r\n            print(f\"Error in translation: {e}\")\r\n            return \"\"\r\n\r\n    async def translate(self, texts, original_lang, target_lang):\r\n        \"\"\"异步批量翻译\"\"\"\r\n        async with aiohttp.ClientSession() as session:\r\n            tasks = [\r\n                self.translate_single(session, text, original_lang, target_lang)\r\n                for text in texts\r\n            ]\r\n            return await asyncio.gather(*tasks)\r\n\r\nclass Grok_translation:\r\n    def __init__(self):\r\n        config = load_config.load_config()\r\n        # 修改键名为大写的'Grok'以匹配其他API命名风格\r\n        self.api_key = config['translation_services']['Grok']['auth_key']\r\n        self.url = \"https://api-proxy.me/xai/v1/chat/completions\"\r\n        self.headers = {\r\n            \"Content-Type\": \"application/json\",\r\n            \"X-Api-Key\": self.api_key,\r\n            \"Authorization\": f\"Bearer {self.api_key}\"\r\n        }\r\n        self.model = config['translation_services']['Grok']['model_name']\r\n        # 从配置中读取翻译提示词\r\n        self.prompt_template = config.get('translation_prompt', {}).get('system_prompt', \r\n            'You are a professional translator. Translate from {original_lang} to {target_lang}. Return only the translation without explanations or notes.')\r\n\r\n    async def translate_single(self, session, text, original_lang, target_lang):\r\n        \"\"\"单个文本的异步翻译\"\"\"\r\n        payload = {\r\n            \"model\": self.model,\r\n            \"messages\": [\r\n                {\r\n                    \"role\": \"system\",\r\n                    \"content\": self.prompt_template.format(original_lang=original_lang, target_lang=target_lang)\r\n                },\r\n                {\r\n                    \"role\": \"user\",\r\n                    \"content\": text\r\n                }\r\n            ],\r\n            \"temperature\": 0.3,\r\n            \"stream\": False\r\n        }\r\n\r\n        try:\r\n            async with session.post(self.url, headers=self.headers, json=payload) as response:\r\n                if response.status == 200:\r\n                    result = await response.json()\r\n                    translated_text = result['choices'][0]['message']['content'].strip()\r\n                    # 添加调试日志\r\n                    #print(f\"Grok translated: '{text[:30]}...' -> '{translated_text[:30]}...'\")\r\n                    return translated_text\r\n                else:\r\n                    error_text = await response.text()\r\n                    print(f\"Error: {response.status}, Details: {error_text}\")\r\n                    return \"\"\r\n        except Exception as e:\r\n            print(f\"Error in translation: {e}\")\r\n            return \"\"\r\n\r\n    async def translate(self, texts, original_lang, target_lang):\r\n        \"\"\"异步批量翻译\"\"\"\r\n        print(f\"Starting Grok translation of {len(texts)} texts\")\r\n        async with aiohttp.ClientSession() as session:\r\n            tasks = [\r\n                self.translate_single(session, text, original_lang, target_lang)\r\n                for text in texts\r\n            ]\r\n            results = await asyncio.gather(*tasks)\r\n            print(f\"Grok translation completed, {len(results)} texts translated\")\r\n            return results\r\n\r\nclass ThirdParty_translation:\r\n    def __init__(self):\r\n        config = load_config.load_config()\r\n        self.api_key = config['translation_services']['ThirdParty']['auth_key']\r\n        self.url = config['translation_services']['ThirdParty']['api_url']\r\n        self.headers = {\r\n            \"Authorization\": f\"Bearer {self.api_key}\",\r\n            \"Content-Type\": \"application/json\"\r\n        }\r\n        self.model = config['translation_services']['ThirdParty']['model_name']\r\n        # 从配置中读取翻译提示词\r\n        self.prompt_template = config.get('translation_prompt', {}).get('system_prompt', \r\n            'You are a professional translator. Translate from {original_lang} to {target_lang}. Return only the translation without explanations or notes.')\r\n\r\n    async def translate_single(self, session, text, original_lang, target_lang):\r\n        \"\"\"单个文本的异步翻译\"\"\"\r\n        payload = {\r\n            \"model\": self.model,\r\n            \"messages\": [\r\n                {\r\n                    \"role\": \"system\",\r\n                    \"content\": self.prompt_template.format(original_lang=original_lang, target_lang=target_lang)\r\n                },\r\n                {\r\n                    \"role\": \"user\",\r\n                    \"content\": text\r\n                }\r\n            ],\r\n            \"temperature\": 0.3,\r\n            \"stream\": False\r\n        }\r\n\r\n        try:\r\n            async with session.post(self.url, headers=self.headers, json=payload) as response:\r\n                if response.status == 200:\r\n                    result = await response.json()\r\n                    translated_text = result['choices'][0]['message']['content'].strip()\r\n                    # 添加调试日志\r\n                    #print(f\"ThirdParty translated: '{text[:30]}...' -> '{translated_text[:30]}...'\")\r\n                    return translated_text\r\n                else:\r\n                    error_text = await response.text()\r\n                    print(f\"Error: {response.status}, Details: {error_text}\")\r\n                    return \"\"\r\n        except Exception as e:\r\n            print(f\"Error in translation: {e}\")\r\n            return \"\"\r\n\r\n    async def translate(self, texts, original_lang, target_lang):\r\n        \"\"\"异步批量翻译\"\"\"\r\n        print(f\"Starting ThirdParty translation of {len(texts)} texts\")\r\n        async with aiohttp.ClientSession() as session:\r\n            tasks = [\r\n                self.translate_single(session, text, original_lang, target_lang)\r\n                for text in texts\r\n            ]\r\n            results = await asyncio.gather(*tasks)\r\n            print(f\"ThirdParty translation completed, {len(results)} texts translated\")\r\n            return results\r\n\r\nclass GLM_translation:\r\n    def __init__(self):\r\n        config = load_config.load_config()\r\n        self.api_key = config['translation_services']['GLM']['auth_key']\r\n        self.url = \"https://open.bigmodel.cn/api/paas/v4/chat/completions\"\r\n        self.headers = {\r\n            \"Authorization\": f\"Bearer {self.api_key}\",\r\n            \"Content-Type\": \"application/json\"\r\n        }\r\n        self.model = config['translation_services']['GLM']['model_name']\r\n        # 从配置中读取翻译提示词\r\n        self.prompt_template = config.get('translation_prompt', {}).get('system_prompt', \r\n            'You are a professional translator. Translate from {original_lang} to {target_lang}. Return only the translation without explanations or notes.')\r\n\r\n    async def translate_single(self, session, text, original_lang, target_lang):\r\n        \"\"\"单个文本的异步翻译\"\"\"\r\n        # 过滤控制字符，解决GLM 1213错误\r\n        cleaned_text = re.sub(r'[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F-\\x9F]', '', text)\r\n        if not cleaned_text.strip():\r\n            return \"\"\r\n            \r\n        payload = {\r\n            \"model\": self.model,\r\n            \"messages\": [\r\n                {\r\n                    \"role\": \"system\",\r\n                    \"content\": self.prompt_template.format(original_lang=original_lang, target_lang=target_lang)\r\n                },\r\n                {\r\n                    \"role\": \"user\",\r\n                    \"content\": cleaned_text\r\n                }\r\n            ],\r\n            \"temperature\": 0.3,\r\n            \"top_p\": 0.7,\r\n            \"do_sample\": False\r\n        }\r\n\r\n        try:\r\n            async with session.post(self.url, headers=self.headers, json=payload) as response:\r\n                if response.status == 200:\r\n                    result = await response.json()\r\n                    return result['choices'][0]['message']['content'].strip()\r\n                else:\r\n                    error_text = await response.text()\r\n                    print(f\"Error: {response.status}, Details: {error_text}\")\r\n                    return \"\"\r\n        except Exception as e:\r\n            print(f\"Error in GLM translation: {e}\")\r\n            return \"\"\r\n\r\n    async def translate(self, texts, original_lang, target_lang):\r\n        \"\"\"异步批量翻译\"\"\"\r\n        print(f\"Starting GLM translation of {len(texts)} texts\")\r\n        async with aiohttp.ClientSession() as session:\r\n            tasks = [\r\n                self.translate_single(session, text, original_lang, target_lang)\r\n                for text in texts\r\n            ]\r\n            results = await asyncio.gather(*tasks)\r\n            print(f\"GLM translation completed, {len(results)} texts translated\")\r\n            return results\r\n\r\n# 测试代码\r\nasync def main():\r\n    texts = [\r\n        \"Hello, how are you?\",\r\n        \"What's the weather like today?\",\r\n        \"I love programming\",\r\n        \"Python is awesome\",\r\n        \"Machine learning is interesting\"\r\n    ]\r\n\r\n    # 302.ai翻译测试\r\n    ai302_translator = AI302_translation()\r\n    translated_ai302 = await ai302_translator.translate(\r\n        texts=texts,\r\n        original_lang=\"en\",\r\n        target_lang=\"zh\"\r\n    )\r\n    print(\"302.ai translations:\")\r\n    for src, tgt in zip(texts, translated_ai302):\r\n        print(f\"{src} -> {tgt}\")\r\n\r\n    # OpenAI翻译测试\r\n    openai_translator = Openai_translation()\r\n    translated_openai = await openai_translator.translate(\r\n        texts=texts,\r\n        original_lang=\"en\",\r\n        target_lang=\"zh\"\r\n    )\r\n    print(\"OpenAI translations:\")\r\n    for src, tgt in zip(texts, translated_openai):\r\n        print(f\"{src} -> {tgt}\")\r\n\r\n    # DeepSeek翻译测试\r\n    deepseek_translator = Deepseek_translation()\r\n    translated_deepseek = await deepseek_translator.translate(\r\n        texts=texts,\r\n        original_lang=\"en\",\r\n        target_lang=\"zh\"\r\n    )\r\n    print(\"\\nDeepSeek translations:\")\r\n    for src, tgt in zip(texts, translated_deepseek):\r\n        print(f\"{src} -> {tgt}\")\r\n\r\n    qwen_translator = Qwen_translation()\r\n    translated_deepseek = await qwen_translator.translate(\r\n        texts=texts,\r\n        original_lang=\"en\",\r\n        target_lang=\"zh\"\r\n    )\r\n    print(\"\\nQwen translations:\")\r\n    for src, tgt in zip(texts, translated_deepseek):\r\n        print(f\"{src} -> {tgt}\")\r\n\r\n    # Grok翻译测试\r\n    grok_translator = Grok_translation()\r\n    translated_grok = await grok_translator.translate(\r\n        texts=texts,\r\n        original_lang=\"en\",\r\n        target_lang=\"zh\"\r\n    )\r\n    print(\"\\nGrok translations:\")\r\n    for src, tgt in zip(texts, translated_grok):\r\n        print(f\"{src} -> {tgt}\")\r\n\r\n    # 添加ThirdParty翻译测试\r\n    try:\r\n        thirdparty_translator = ThirdParty_translation()\r\n        translated_thirdparty = await thirdparty_translator.translate(\r\n            texts=texts,\r\n            original_lang=\"en\",\r\n            target_lang=\"zh\"\r\n        )\r\n        print(\"\\nThirdParty translations:\")\r\n        for src, tgt in zip(texts, translated_thirdparty):\r\n            print(f\"{src} -> {tgt}\")\r\n    except Exception as e:\r\n        print(f\"Error testing ThirdParty translation: {e}\")\r\n\r\n    # 添加 GLM 翻译测试\r\n    try:\r\n        glm_translator = GLM_translation()\r\n        translated_glm = await glm_translator.translate(\r\n            texts=texts,\r\n            original_lang=\"en\",\r\n            target_lang=\"zh\"\r\n        )\r\n        print(\"\\nGLM translations:\")\r\n        for src, tgt in zip(texts, translated_glm):\r\n            print(f\"{src} -> {tgt}\")\r\n    except Exception as e:\r\n        print(f\"Error testing GLM translation: {e}\")\r\n\r\n    # 添加Bing翻译测试\r\n    try:\r\n        bing_translator = Bing_translation()\r\n        translated_bing = await bing_translator.translate(\r\n            texts=texts,\r\n            original_lang=\"en\",\r\n            target_lang=\"zh\"\r\n        )\r\n        print(\"\\nBing translations:\")\r\n        for src, tgt in zip(texts, translated_bing):\r\n            print(f\"{src} -> {tgt}\")\r\n    except Exception as e:\r\n        print(f\"Error testing Bing translation: {e}\")\r\n\r\nif __name__ == \"__main__\":\r\n    asyncio.run(main())\r\n"
  },
  {
    "path": "OldMain.py",
    "content": "import math\nimport All_Translation as at\nfrom PIL import Image\nimport pytesseract\nimport time\nimport fitz\nimport os\nimport unicodedata\nimport download_model\nimport load_config\nimport re\nfrom datetime import datetime\nimport pdf_thumbnail\nfrom load_config import APP_DATA_DIR\nconfig = load_config.load_config()\ntranslation_type = config['default_services']['Translation_api']\ntranslation = config['default_services']['Enable_translation']\nuse_mupdf = not config['default_services']['ocr_model']\n\nPPC = config['PPC']\nprint('ppc',PPC)\nline_model = config['default_services']['line_model']\nprint('line',line_model)\n# print(use_mupdf,'mupdf值')\n# print('当前',config['count'])\n\n\n\ndef get_font_by_language(target_language):\n    font_mapping = {\n        'zh': \"'Microsoft YaHei', 'SimSun'\",  # 中文\n        'en': \"'Times New Roman', Arial\",      # 英文\n        'ja': \"'MS Mincho', 'Yu Mincho'\",     # 日文\n        'ko': \"'Malgun Gothic'\",              # 韩文\n    }\n    # 如果找不到对应语言，返回默认字体\n    return font_mapping.get(target_language, \"'Times New Roman', Arial\")\n\n\ndef is_math(text, page_num,font_info):\n    \"\"\"\n    判断文本是否为非文本（如数学公式或者长度小于4的文本）\n    \"\"\"\n\n\n    # 判断文本长度\n    # print('文本为:',text)\n    text_len = len(text)\n    if text_len < 4:\n        return True\n    math_fonts = [\n        # Computer Modern Math\n        'CMMI', 'CMSY', 'CMEX',\n        'CMMI5', 'CMMI6', 'CMMI7', 'CMMI8', 'CMMI9', 'CMMI10',\n        'CMSY5', 'CMSY6', 'CMSY7', 'CMSY8', 'CMSY9', 'CMSY10',\n\n        # AMS Math\n        'MSAM', 'MSBM', 'EUFM', 'EUSM',\n\n        # Times/Palatino Math\n        'TXMI', 'TXSY', 'PXMI', 'PXSY',\n\n        # Modern Math\n        'CambriaMath', 'AsanaMath', 'STIXMath', 'XitsMath',\n        'Latin Modern Math', 'Neo Euler'\n    ]\n    # 检查文本长度是否小于50且字体是否在数学字体列表中\n    text_len_non_sp = len(text.replace(\" \", \"\"))\n\n    if text_len < 70 and any(math_font in font_info for math_font in math_fonts):\n        # print(text,'小于50且字体')\n        return True\n\n    if 15 < text_len_non_sp <100:\n        # 使用正则表达式找出所有5个或更多任意字符连续组成的单词\n        long_words = re.findall(r'\\S{5,}', text)\n        if len(long_words) < 2:\n            # print(text_len)\n            # print(text, '15 < text_len <100')\n            return True\n\n\n    # 分行处理\n    lines = text.split('\\n')\n    len_lines = len([line for line in lines if line.strip()])\n\n    # 找到长度最小和最大的行\n    min_line_len = min((len(line) for line in lines if line.strip()), default=text_len)\n    max_line_len = max((len(line) for line in lines), default=text_len)\n\n    # 计算空格比例\n    newline_count = text.count('\\n')\n    total_spaces = text.count(' ') + (newline_count * 5)\n    space_ratio = total_spaces / text_len if text_len > 0 else 0\n\n    # 定义数学符号集合\n    math_symbols = \"=∑θ∫∂√±ΣΠfδλσε∋∈µ→()|−ˆ,.+*/[]{}^_<>~#%&@!?;:'\\\"\\\\-\"\n\n    # 检查是否存在完整单词(5个或更多非数学符号的连续字符)\n    text_no_spaces = text.replace(\" \", \"\")\n\n    # 创建一个正则表达式，匹配5个或更多连续的非数学符号字符\n    pattern = r'[^' + re.escape(math_symbols) + r']{5,}'\n    has_complete_word = bool(re.search(pattern, text_no_spaces))\n\n    # 如果没有完整单词，认为是非文本\n    if not has_complete_word:\n        # print(text, '没有完整单词')\n        return True\n\n\n    # 计算数字占比\n    digit_count = sum(c.isdigit() for c in text)\n    digit_ratio = digit_count / text_len if text_len > 0 else 0\n\n    # 如果数字占比超过30%，返回True\n    if digit_ratio > 0.3:\n        # print(text, '数字占比超过30%')\n        return True\n\n\n\n\n\n    # 检查数学公式\n    math_symbols = set(\"=∑θ∫∂√±ΣΠδλσε∋∈µ→()|−ˆ,...\")\n    # 数学公式判断条件2:包含至少2个数学符号且总文本较短\n    if sum(1 for sym in math_symbols if sym in text) >= 2 and len(text_no_spaces) < 25:\n        # found_symbols = [sym for sym in text if sym in math_symbols]\n        # print(f\"在文本中找到的数学符号: {found_symbols}\")\n        # print(sum(1 for sym in math_symbols if sym in text))\n        # print(text, '条件2')\n        return True\n\n    # 数学公式判断条件1:包含至少2个数学符号且行短且行数少且最大行长度小\n    if sum(1 for sym in math_symbols if sym in text) >= 2 and min_line_len < 10 and len_lines < 5 and max_line_len < 35:\n        # print(text, '条件1')\n        return True\n\n    # 数学公式判断条件3:包含至少2个数学符号且空格比例高\n    if sum(1 for sym in math_symbols if sym in text) >= 2 and space_ratio > 0.5:\n        # print(text, '条件3')\n        return True\n    # 数学公式判断条件4:包含至少1个数学符号且空格数高\n    if sum(1 for sym in math_symbols if sym in text) >= 1 and total_spaces > 3 and text_len<20:\n        # print(text, '条件4')\n        return True\n\n    return False\n\ndef line_non_text(text):\n    \"\"\"\n    判断文本是否由纯数字和所有(Unicode)标点符号组成\n    参数：\n        text: 待检查的文本\n    返回：\n        bool: 如果文本由纯数字和标点符号组成返回True，否则返回False\n    \"\"\"\n    text = text.strip()\n    if not text:\n        return False\n    for ch in text:\n        # 使用 unicodedata.category() 获取字符在 Unicode 标准中的分类\n        # 'Nd' 代表十进制数字 (Number, Decimal digit)\n        # 'P' 代表各种标点 (Punctuation)，如 Po, Ps, Pe, 等\n        cat = unicodedata.category(ch)\n        if not (cat == 'Nd' or cat.startswith('P')):\n            return False\n    return True\n\n\ndef is_non_text(text):\n    \"\"\"\n    判断是否为参考文献格式\n    参数：\n    text: 待检查的文本\n    返回：\n    bool: 如果是参考文献格式返回True，否则返回False\n    \"\"\"\n    # 去除开头的空白字符\n    text = text.lstrip()\n\n    # 检查是否以[数字]开头\n    pattern = r'^\\[\\d+\\]'\n\n    if re.match(pattern, text):\n        return True\n\n    return False\nfont_collection = []\n\n\nclass main_function:\n    def __init__(self, pdf_path,\n                 original_language, target_language,bn = None,en = None,\n                 DPI=72,):\n        \"\"\"\n        这里的参数与原来保持一致或自定义。主要多加一个 self.pages_data 用于存储所有页面的提取结果。\n        \"\"\"\n\n        self.pdf_path = pdf_path\n        self.pdf_path = pdf_path\n        self.full_path = os.path.join(APP_DATA_DIR, 'static', 'original', pdf_path)\n        self.doc = fitz.open(self.full_path)\n\n        self.original_language = original_language\n        self.target_language = target_language\n        self.DPI = DPI\n        self.translation = translation\n        self.translation_type = translation_type\n        self.use_mupdf = use_mupdf\n        self.line_model = line_model\n        self.bn = bn\n        self.en = en\n\n        self.t = time.time()\n        # 新增一个全局列表，用于存所有页面的 [文本, bbox]，以及翻译后结果\n        # 形式: self.pages_data[page_index] = [ [原文, bbox], [原文, bbox], ... ]\n        self.pages_data = []\n\n    def main(self):\n        \"\"\"\n        主流程函数。只做“计数更新、生成缩略图、建条目”等老逻辑，替换原来在这里的逐页翻译写入。\n        但是保留 if use_mupdf: for... self.start(...) else: for... self.start(...)\n        不做“翻译和写入”的动作，而是只做“提取文本”。\n        提取完所有页面后，批量翻译，再统一写入 PDF。\n        \"\"\"\n        # 1. 计数和配置信息\n        load_config.update_count()\n        config = load_config.load_config()\n        count = config[\"count\"]\n\n\n        # 2. 生成 PDF 缩略图 (保留原逻辑)\n        pdf_thumbnail.create_pdf_thumbnail(self.full_path, width=400)\n\n        # 3. 创建新条目（保留原逻辑）\n        current_time = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n        new_entry = {\n            \"index\": count,\n            \"date\": current_time,\n            \"name\": self.pdf_path,\n            \"original_language\": self.original_language,\n            \"target_language\": self.target_language,\n            \"read\": \"0\",\n            \"statue\": \"0\"\n        }\n        load_config.add_new_entry(new_entry)\n\n        # 4. 保留原先判断是否 use_mupdf 的代码，以便先提取文本\n        page_count =self.doc.page_count\n        if self.bn == None:\n            self.bn = 0\n        if self.en == None:\n            self.en = page_count\n\n        if self.use_mupdf:\n            start_page = self.bn\n            end_page = min(self.en, page_count)\n\n            # 使用 PyMuPDF 直接获取文本块\n            for i in range(start_page, end_page):\n                self.start(image=None, pag_num=i)  # 只做提取，不做翻译写入\n        else:\n            # OCR 模式\n            zoom = self.DPI / 72\n            mat = fitz.Matrix(zoom, zoom)\n            # 处理从 self.bn 到 self.en 的页面范围，并确保 self.en 不超过文档页数\n            start_page = self.bn\n            end_page = min(self.en, page_count)\n\n            # 迭代指定范围的页面\n            for i in range(start_page, end_page):\n                page = self.doc[i]  # 获取指定页面\n                pix = page.get_pixmap(matrix=mat)\n                image = Image.frombytes(\"RGB\", [pix.width, pix.height], pix.samples)\n                # 如果需要保存图像到文件，可自行保留或注释\n                # image.save(f'page_{i}.jpg', 'JPEG')\n                self.start(image=image, pag_num=i)  # 只做提取，不做翻译写入\n\n        # 5. 若开启翻译，则批量翻译所有提取的文本\n\n        self.batch_translate_pages_data(\n                original_language=self.original_language,\n                target_language=self.target_language,\n                translation_type=self.translation_type,\n                batch_size=PPC\n            )\n\n        # 6. 将翻译结果统一写入 PDF（覆盖+插入译文）\n        self.apply_translations_to_pdf()\n\n        # 7. 保存 PDF、更新状态\n        pdf_name, _ = os.path.splitext(self.pdf_path)\n        target_path = os.path.join(APP_DATA_DIR, 'static', 'target', f\"{pdf_name}_{self.target_language}.pdf\")\n        self.doc.ez_save(\n            target_path,\n            garbage=4,\n            deflate=True\n        )\n\n        load_config.update_file_status(count, statue=\"1\")  # statue = \"1\"\n\n        # 8. 打印耗时\n        end_time = time.time()\n        print(end_time - self.t)\n\n    def start(self, image, pag_num):\n        \"\"\"\n        原先逐页处理的函数，现仅负责“提取文本并存储在 self.pages_data[pag_num]”。\n        不在这里直接翻译或写回 PDF。\n        \"\"\"\n        # 确保 self.pages_data 有 pag_num 对应的列表\n        while len(self.pages_data) <= pag_num:\n            self.pages_data.append([])  # 每个元素是 [ [text, (x0,y0,x1,y1)], ... ]\n\n        page = self.doc.load_page(pag_num)\n\n        if self.line_model and self.use_mupdf:\n            def snap_angle_func(angle):\n                \"\"\"\n                将任意角度自动映射到 0、90、180、270 四个值之一。\n                \"\"\"\n                # 将角度映射到 [0, 360) 区间\n                angle = abs(angle) % 360\n                # 选取最接近的标准角度\n                possible_angles = [0, 90, 180, 270]\n                return min(possible_angles, key=lambda x: abs(x - angle))\n\n            blocks = page.get_text(\"dict\")[\"blocks\"]\n            for block in blocks:\n                if block.get(\"type\") == 0:  # 文本块\n                    font_info = None\n                    # 遍历每一行\n                    for line in block[\"lines\"]:\n                        # 1) 拼接文本（减少反复 += 操作）\n                        span_texts = [span[\"text\"] for span in line[\"spans\"] if \"text\" in span]\n                        line_text = \"\".join(span_texts).strip()\n\n                        # 2) 如果行文本为空或仅含数字标点，就跳过\n                        if not line_text or line_non_text(line_text):\n                            continue\n\n                        # 3) 此时才计算旋转角度，避免空行浪费\n                        direction = line.get(\"dir\", [1.0, 0.0])\n                        raw_angle = math.degrees(math.atan2(direction[1], direction[0]))\n                        angle = snap_angle_func(raw_angle)\n\n                        # 4) 只在需要时提取字体信息\n                        if not font_info:\n                            for span in line[\"spans\"]:\n                                if \"font\" in span:\n                                    font_info = span[\"font\"]\n                                    break\n                        if font_info and font_info not in font_collection:\n                            font_collection.append(font_info)\n\n                        line_bbox = line.get(\"bbox\")\n                        # 5) 加入提取结果\n                        self.pages_data[pag_num].append([\n                            line_text,  # 原文\n                            tuple(line_bbox),  # BBox\n                            None,  # 预留翻译文本\n                            angle  # 行角度\n                        ])\n\n\n        # 如果用 PyMuPDF 提取文字\n        elif self.use_mupdf and image is None:\n            blocks = page.get_text(\"dict\")[\"blocks\"]\n            for block in blocks:\n                if block.get(\"type\") == 0:  # 文本块\n                    bbox = block[\"bbox\"]\n                    text = \"\"\n                    font_info = None\n                    for line in block[\"lines\"]:\n                        for span in line[\"spans\"]:\n                            span_text = span[\"text\"].strip()\n                            if span_text:\n                                text += span_text + \" \"\n                                if not font_info and \"font\" in span:\n                                    font_info = span[\"font\"]\n                    text = text.strip()\n                    if text and not is_math(text, pag_num, font_info) and not is_non_text(text):\n                        self.pages_data[pag_num].append([text, tuple(bbox),None])\n\n        else:\n            # OCR 提取文字\n            config = load_config.load_config()\n            tesseract_path = config['ocr_services']['tesseract']['path']\n            pytesseract.pytesseract.tesseract_cmd = tesseract_path\n\n            Full_width, Full_height = image.size\n            ocr_result = pytesseract.image_to_data(image, output_type=pytesseract.Output.DICT)\n\n            current_paragraph_text = ''\n            paragraph_bbox = {\n                'left': float('inf'),\n                'top': float('inf'),\n                'right': 0,\n                'bottom': 0\n            }\n            current_block_num = None\n            Threshold_width = 0.06 * Full_width\n            Threshold_height = 0.006 * Full_height\n\n            for i in range(len(ocr_result['text'])):\n                block_num = ocr_result['block_num'][i]\n                text_ocr = ocr_result['text'][i].strip()\n                left = ocr_result['left'][i]\n                top = ocr_result['top'][i]\n                width = ocr_result['width'][i]\n                height = ocr_result['height'][i]\n\n                if text_ocr and not is_math(text_ocr, pag_num, font_info='22') and not is_non_text(text_ocr):\n\n                    # 若换 block 或段落间隔较大，则保存上一段\n                    if (block_num != current_block_num or\n                       (abs(left - paragraph_bbox['right']) > Threshold_width and\n                        abs(height - (paragraph_bbox['bottom'] - paragraph_bbox['top'])) > Threshold_height and\n                        abs(left - paragraph_bbox['left']) > Threshold_width)):\n\n                        if current_paragraph_text:\n                            # 转换到 PDF 坐标\n                            Full_rect = page.rect\n                            w_points = Full_rect.width\n                            h_points = Full_rect.height\n\n                            x0_ratio = paragraph_bbox['left'] / Full_width\n                            y0_ratio = paragraph_bbox['top'] / Full_height\n                            x1_ratio = paragraph_bbox['right'] / Full_width\n                            y1_ratio = paragraph_bbox['bottom'] / Full_height\n\n                            x0_pdf = x0_ratio * w_points\n                            y0_pdf = y0_ratio * h_points\n                            x1_pdf = x1_ratio * w_points\n                            y1_pdf = y1_ratio * h_points\n\n                            self.pages_data[pag_num].append([\n                                current_paragraph_text.strip(),\n                                (x0_pdf, y0_pdf, x1_pdf, y1_pdf)\n                            ])\n\n                        # 重置\n                        current_paragraph_text = ''\n                        paragraph_bbox = {\n                            'left': float('inf'),\n                            'top': float('inf'),\n                            'right': 0,\n                            'bottom': 0\n                        }\n                        current_block_num = block_num\n\n                    # 继续累加文本\n                    current_paragraph_text += text_ocr + \" \"\n                    paragraph_bbox['left'] = min(paragraph_bbox['left'], left)\n                    paragraph_bbox['top'] = min(paragraph_bbox['top'], top)\n                    paragraph_bbox['right'] = max(paragraph_bbox['right'], left + width)\n                    paragraph_bbox['bottom'] = max(paragraph_bbox['bottom'], top + height)\n\n            # 收尾：最后一段存入\n            if current_paragraph_text:\n                Full_rect = page.rect\n                w_points = Full_rect.width\n                h_points = Full_rect.height\n\n                x0_ratio = paragraph_bbox['left'] / Full_width\n                y0_ratio = paragraph_bbox['top'] / Full_height\n                x1_ratio = paragraph_bbox['right'] / Full_width\n                y1_ratio = paragraph_bbox['bottom'] / Full_height\n\n                x0_pdf = x0_ratio * w_points\n                y0_pdf = y0_ratio * h_points\n                x1_pdf = x1_ratio * w_points\n                y1_pdf = y1_ratio * h_points\n\n                self.pages_data[pag_num].append([\n                    current_paragraph_text.strip(),\n                    (x0_pdf, y0_pdf, x1_pdf, y1_pdf),\n                    None\n                ])\n\n        # 注意：这里不做翻译、不插入 PDF，只负责“收集文本”到 self.pages_data\n\n    def batch_translate_pages_data(self, original_language, target_language,\n                                   translation_type, batch_size=PPC ):\n        \"\"\"PPC (Pages Per Call)\n        分批翻译 pages_data，每次处理最多 batch_size 页的文本，避免一次性过多。\n        将译文存回 self.pages_data 的第三个元素，如 [原文, bbox, 译文]\n        \"\"\"\n        total_pages = len(self.pages_data)\n        start_idx = 0\n\n        while start_idx < total_pages:\n            end_idx = min(start_idx + batch_size, total_pages)\n\n            # 收集该批次的所有文本\n            batch_texts = []\n            for i in range(start_idx, end_idx):\n                for block in self.pages_data[i]:\n                    batch_texts.append(block[0])  # block[0] = 原文\n\n            # 翻译\n\n\n            if self.translation and use_mupdf:\n                translation_list = at.Online_translation(\n                    original_language=original_language,\n                    target_language=target_language,\n                    translation_type=translation_type,\n                    texts_to_process=batch_texts\n                ).translation()\n            elif self.translation and not use_mupdf:\n                # 离线翻译\n                translation_list = at.Offline_translation(\n                    original_language=original_language,\n                    target_language=target_language,\n                    texts_to_process=batch_texts\n                ).translation()\n            else:\n\n                translation_list = batch_texts\n\n\n\n            # 回填译文\n            idx_t = 0\n            for i in range(start_idx, end_idx):\n                for block in self.pages_data[i]:\n                    # 在第三个位置添加翻译文本\n                    block[2] = translation_list[idx_t]\n                    idx_t += 1\n\n            start_idx += batch_size\n            print('当前进度', end_idx, \"/\", total_pages)\n\n    def apply_translations_to_pdf(self):\n        \"\"\"\n        统一对 PDF 做“打码/打白 + 插入译文”操作\n        \"\"\"\n        for page_index, blocks in enumerate(self.pages_data):\n            page = self.doc.load_page(page_index)\n\n            for block in blocks:\n                original_text = block[0]\n                coords = block[1]  # (x0, y0, x1, y1)\n                # 如果第三个元素是译文，则用之，否则用原文\n                translated_text = block[2] if len(block) >= 3 else original_text\n\n                if self.line_model:\n                    angle = block[3] if len(block) > 3 else 0\n\n                else:\n                    angle = 0\n\n                rect = fitz.Rect(*coords)\n\n                # 先尝试使用 Redact 遮盖\n                try:\n                    page.add_redact_annot(rect)\n                    page.apply_redactions()\n                except Exception as e:\n                    # 若 Redact 失败，改用白色方块覆盖\n                    annots = list(page.annots() or [])\n                    if annots:\n                        page.delete_annot(annots[-1])\n                    try:\n                        page.draw_rect(rect, color=(1, 1, 1), fill=(1, 1, 1))\n                    except Exception as e2:\n                        print(f\"创建白色画布时发生错误: {e2}\")\n                    print(f\"应用重编辑时发生错误: {e}\")\n\n\n\n                page.insert_htmlbox(\n                    rect,\n                    translated_text,\n                    css=\"\"\"\n                * {\n                    font-family: \"Microsoft YaHei\";\n                    /* 这行可把内容改成粗体, 可写 \"bold\" 或数字 (100–900) */\n                    font-weight: 100;\n                    /* 这里可以使用标准 CSS 颜色写法, 例如 #FF0000、rgb() 等 */\n                    color:  #333333;\n                }\n                \"\"\",\n                    rotate=angle\n\n                )\n\n\n        # 完成后不会立即保存，需要在 main(...) 里 self.doc.ez_save(...)\n\nif __name__ == '__main__':\n\n    main_function(original_language='auto', target_language='zh', pdf_path='g2.pdf').main()"
  },
  {
    "path": "README.md",
    "content": "python包在2.2版本之前预计不会更新，2.2版本预估采取解析最底层span获取更信息的布局逻辑解决，预估解决：行内公式错误判断为公式块，错误将粗体文本进行分段bug,以及insert_html方法重复嵌入字体文件导致处理页数较大pdf时浪费计算资源极其卡顿。 目前效果，对于基于文本的pdf,polyglotpdf的解析方式依旧是最优解。 ocr和布局分析并不总是完美。（考虑处理文本上下标问题，大部分pdf文件中上标下标文本通过指定坐标和字体大小实现伪上下标，考虑替换为真正的上下标文字对应的Unicode编码，但并不完美），对于报告型表格文档，polyglotpdf效果相当完美，当然表格中的复杂矢量数学公式依旧无法正确处理）。\n寻求意见的改进方法，对于复杂的颜色布局文本或者粗体参杂常规字体文本，提出以下方法，对于流内容我们可以解析为html格式如下：\n\n    <p style=\"color: red; display: inline;\">ABSTRACT: </p>\n    <p style=\"display: inline;\">\n        The swine industry annually suffers significant economic losses caused by porcine reproductive and respiratory syndrome virus (PRRSV). Because the available commercial vaccines have limited protective efficacy against epidemic PRRSV, there is an urgent need for innovative solutions. Nanoparticle vaccines induce robust immune responses and have become a promising direction in vaccine development. In this study, we designed and produced a self-assembling nanoparticle vaccine derived from thermophilic archaeal ferritin to combat epidemic PRRSV. First, multiple T cell epitopes targeting viral structural proteins were identified by IFN-γ screening after PRRSV infection. Three different self-assembled nanoparticles with epitopes targeting viral GP3, GP4, and GP5.\n    </p>\n\n  这种解析内容只能由llms翻译，翻译结果如下：\n  ```html\n<p style=\"color: red; display: inline;\">摘要：</p>\n<p style=\"display: inline;\">\n    猪产业每年因猪繁殖与呼吸综合征病毒（PRRSV）造成显著的经济损失。由于现有的商业疫苗对流行性PRRSV的保护效果有限，迫切需要创新的解决方案。纳米粒子疫苗能够引发强烈的免疫反应，已成为疫苗开发的一个有前景的方向。在本研究中，我们设计并生产了一种源自嗜热古细菌铁蛋白的自组装纳米粒子疫苗，以对抗流行性PRRSV。首先，通过PRRSV感染后的IFN-γ筛选，识别出针对病毒结构蛋白的多个T细胞表位。三种不同的自组装纳米粒子携带针对病毒GP3、GP4和GP5的表位。\n</p>\n```\n甚至包括粗体：\n  ```html\n<p style=\"color: blue; font-weight: bold; display: inline;\">摘要：</p>\n<p style=\"display: inline;\">\n    猪产业每年因猪繁殖与呼吸综合征病毒（PRRSV）造成显著的经济损失。由于现有的商业疫苗对流行性PRRSV的保护效果有限，迫切需要创新的解决方案。纳米粒子疫苗能够引发强烈的免疫反应，已成为疫苗开发的一个有前景的方向。在本研究中，我们设计并生产了一种源自嗜热古细菌铁蛋白的自组装纳米粒子疫苗，以对抗流行性PRRSV。首先，通过PRRSV感染后的IFN-γ筛选，识别出针对病毒结构蛋白的多个T细胞表位。三种不同的自组装纳米粒子携带针对病毒GP3、GP4和GP5的表位。\n</p>\n```\n这种方法会无线接近于完美的处理，目前考虑将此方法作为强化功能选用\n\nEnglish | [简体中文](/README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JA.md) | [한국어](README_KO.md)\n# PolyglotPDF\n\n[![Python](https://img.shields.io/badge/python-3.8-blue.svg)](https://www.python.org/)\n[![PDF](https://img.shields.io/badge/pdf-documentation-brightgreen.svg)](https://example.com)\n[![LaTeX](https://img.shields.io/badge/latex-typesetting-orange.svg)](https://www.latex-project.org/)\n[![Translation](https://img.shields.io/badge/translation-supported-yellow.svg)](https://example.com)\n[![Math](https://img.shields.io/badge/math-formulas-red.svg)](https://example.com)\n[![PyMuPDF](https://img.shields.io/badge/PyMuPDF-1.24.0-blue.svg)](https://pymupdf.readthedocs.io/)\n\n\n## Demo\n<img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/demo.gif?raw=true\" width=\"80%\" height=\"40%\">\n\n### [🎬 Watch Full Video](https://github.com/CBIhalsen/PolyglotPDF/blob/main/demo.mp4)\n llms has been added as the translation api of choice, Doubao ,Qwen ,deepseek v3 , gpt4-o-mini are recommended. The color space error can be resolved by filling the white areas in PDF files. The old text to text translation api has been removed.\n\nIn addition, consider adding arxiv search function and rendering arxiv papers after latex translation.\n\n### Pages show\n<div style=\"display: flex; margin-bottom: 20px;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page1.png?raw=true\" width=\"40%\" height=\"20%\" style=\"margin-right: 20px;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page2.jpeg?raw=true\" width=\"40%\" height=\"20%\">\n</div>\n<div style=\"display: flex;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page3.png?raw=true\" width=\"40%\" height=\"20%\" style=\"margin-right: 20px;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page4.png?raw=true\" width=\"40%\" height=\"20%\">\n</div>\n\n\n# LLM API Application\n\n## 302.AI\nAI service aggregation platform supporting multiple international mainstream AI models:\n- Official Website: [302.AI](https://302.ai)\n- Registration: [Sign up with invitation link](https://share.302.ai/JBmCb1) (Use invitation code `JBmCb1` to get $1 bonus)\n- Available Models: GPT-4o, GPT-4o-mini, Claude-3.7-Sonnet, DeepSeek-V3 and more\n- Features: Access multiple AI models with one account, pay-per-use pricing\n\n## Doubao & Deepseek\nApply through Volcengine platform:\n- Application URL: [Volcengine-Doubao](https://www.volcengine.com/product/doubao/)\n- Available Models: Doubao, Deepseek series models\n\n## Tongyi Qwen\nApply through Alibaba Cloud platform:\n- Application URL: [Alibaba Cloud-Tongyi Qwen](https://cn.aliyun.com/product/tongyi?from_alibabacloud=&utm_content=se_1019997984)\n- Available Models: Qwen-Max, Qwen-Plus series models\n\n\n## Overview\nPolyglotPDF is an advanced PDF processing tool that employs specialized techniques for ultra-fast text, table, and formula recognition in PDF documents, typically completing processing within 1 second. It features OCR capabilities and layout-preserving translation, with full document translations usually completed within 10 seconds (speed may vary depending on the translation API provider).\n\n\n## Features\n- **Ultra-Fast Recognition**: Processes text, tables, and formulas in PDFs within ~1 second\n- **Layout-Preserving Translation**: Maintains original document formatting while translating content\n- **OCR Support**: Handles scanned documents efficiently\n- **Text-based PDF**：No GPU required\n- **Quick Translation**: Complete PDF translation in approximately 10 seconds\n- **Flexible API Integration**: Compatible with various translation service providers\n- **Web-based Comparison Interface**: Side-by-side comparison of original and translated documents\n- **Enhanced OCR Capabilities**: Improved accuracy in text recognition and processing\n- **Support for offline translation**: Use smaller translation model\n\n## Installation and Usage\n\n<details>\n  <summary>Standard Installation</summary>\n\n1. Clone the repository:\n```bash\ngit clone https://github.com/CBIhalsen/PolyglotPDF.git\ncd polyglotpdf\n```\n\n2. Install required packages:\n```bash\npip install -r requirements.txt\n```\n3. Configure your API key in config.json. The alicloud translation API is not recommended.\n\n4. Run the application:\n```bash\npython app.py\n```\n\n5. Access the web interface:\nOpen your browser and navigate to `http://127.0.0.1:8000`\n</details>\n\n<details>\n  <summary>Docker Installation</summary>\n\n## Quick Start Without Persistence\n\nIf you want to quickly test PolyglotPDF without setting up persistent directories:\n\n```bash\n# Pull the image first\ndocker pull 2207397265/polyglotpdf:latest\n\n# Run container without mounting volumes (data will be lost when container is removed)\ndocker run -d -p 12226:12226 --name polyglotpdf 2207397265/polyglotpdf:latest\n```\n\nThis is the fastest way to try PolyglotPDF, but all uploaded PDFs and configuration changes will be lost when the container stops.\n\n## Installation with Persistent Storage\n\n```bash\n# Create necessary directories\nmkdir -p config fonts static/original static/target static/merged_pdf\n\n# Create config file\nnano config/config.json    # or use any text editor\n# Copy configuration template from the project into this file\n# Make sure to fill in your API keys and other configuration details\n\n# Set permissions\nchmod -R 755 config fonts static\n```\n\n### Quick Start\n\nUse the following commands to pull and run the PolyglotPDF Docker image:\n\n```bash\n# Pull image\ndocker pull 2207397265/polyglotpdf:latest\n\n# Run container\ndocker run -d -p 12226:12226 --name polyglotpdf \\\n  -v ./config/config.json:/app/config.json \\\n  -v ./fonts:/app/fonts \\\n  -v ./static/original:/app/static/original \\\n  -v ./static/target:/app/static/target \\\n  -v ./static/merged_pdf:/app/static/merged_pdf \\\n  2207397265/polyglotpdf:latest\n```\n\n### Access the Application\n\nAfter the container starts, open in your browser:\n```\nhttp://localhost:12226\n```\n\n### Using Docker Compose\n\nCreate a `docker-compose.yml` file:\n\n```yaml\nversion: '3'\nservices:\n  polyglotpdf:\n    image: 2207397265/polyglotpdf:latest\n    ports:\n      - \"12226:12226\"\n    volumes:\n      - ./config.json:/app/config.json # Configuration file\n      - ./fonts:/app/fonts # Font files\n      - ./static/original:/app/static/original # Original PDFs\n      - ./static/target:/app/static/target # Translated PDFs\n      - ./static/merged_pdf:/app/static/merged_pdf # Merged PDFs\n    restart: unless-stopped\n```\n\nThen run:\n\n```bash\ndocker-compose up -d\n```\n\n### Common Docker Commands\n\n```bash\n# Stop container\ndocker stop polyglotpdf\n\n# Restart container\ndocker restart polyglotpdf\n\n# View logs\ndocker logs polyglotpdf\n```\n</details>\n\n## Requirements\n- Python 3.8+\n- deepl==1.17.0\n- Flask==2.0.1\n- Flask-Cors==5.0.0\n- langdetect==1.0.9\n- Pillow==10.2.0\n- PyMuPDF==1.24.0\n- pytesseract==0.3.10\n- requests==2.31.0\n- tiktoken==0.6.0\n- Werkzeug==2.0.1\n\n## Acknowledgments\nThis project leverages PyMuPDF's capabilities for efficient PDF processing and layout preservation.\n\n## Upcoming Improvements\n- PDF chat functionality\n- Academic PDF search integration\n- Optimization for even faster processing speeds\n\n### Known Issues\n- **Issue Description**: Error during text re-editing: `code=4: only Gray, RGB, and CMYK colorspaces supported`\n- **Symptom**: Unsupported color space encountered during text block editing\n- **Current Workaround**: Skip text blocks with unsupported color spaces\n- **Proposed Solution**: Switch to OCR mode for entire pages containing unsupported color spaces\n- **Example**: [View PDF sample with unsupported color spaces](https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/colorspace_issue_sample.pdf)\n\n### TODO\n- □ **Custom Terminology Database**: Support custom terminology databases with prompts for domain-specific professional translation\n- □ **AI Reflow Feature**: Convert double-column PDFs to single-column HTML blog format for easier reading on mobile devices\n- □ **Multi-format Export**: Export translation results to PDF, HTML, Markdown and other formats\n- □ **Multi-device Synchronization**: Read translations on mobile after processing on desktop\n- □ **Enhanced Merge Logic**: Improve the current merge logic by disabling font name detection and enabling horizontal, vertical, x, y range overlap merging\n\n### Font Optimization\nCurrent font configuration in the `start` function of `main.py`:\n```python\n# Current configuration\ncss=f\"* {{font-family:{get_font_by_language(self.target_language)};font-size:auto;color: #111111 ;font-weight:normal;}}\"\n```\n\nYou can optimize font display through the following methods:\n\n1. **Modify Default Font Configuration**\n```python\n# Custom font styles\ncss=f\"\"\"* {{\n    font-family: {get_font_by_language(self.target_language)};\n    font-size: auto;\n    color: #111111;\n    font-weight: normal;\n    letter-spacing: 0.5px;  # Adjust letter spacing\n    line-height: 1.5;      # Adjust line height\n}}\"\"\"\n```\n\n2. **Embed Custom Fonts**\nYou can embed custom fonts by following these steps:\n- Place font files (.ttf, .otf) in the project's `fonts` directory\n- Use `@font-face` to declare custom fonts in CSS\n```python\ncss=f\"\"\"\n@font-face {{\n    font-family: 'CustomFont';\n    src: url('fonts/your-font.ttf') format('truetype');\n}}\n* {{\n    font-family: 'CustomFont', {get_font_by_language(self.target_language)};\n    font-size: auto;\n    font-weight: normal;\n}}\n\"\"\"\n```\n\n### Basic Principles\nThis project follows similar basic principles as Adobe Acrobat DC's PDF editing, using PyMuPDF for text block recognition and manipulation:\n\n- **Core Process**:\n```python\n# Get text blocks from the page\nblocks = page.get_text(\"dict\")[\"blocks\"]\n\n# Process each text block\nfor block in blocks:\n    if block.get(\"type\") == 0:  # text block\n        bbox = block[\"bbox\"]     # get text block boundary\n        text = \"\"\n        font_info = None\n        # Collect text and font information\n        for line in block[\"lines\"]:\n            for span in line[\"spans\"]:\n                text += span[\"text\"] + \" \"\n```\nThis approach directly processes PDF text blocks, maintaining the original layout while achieving efficient text extraction and modification.\n\n- **Technical Choices**:\n  - Utilizes PyMuPDF for PDF parsing and editing\n  - Focuses on text processing\n  - Avoids complex operations like AI formula recognition, table processing, or page restructuring\n\n- **Why Avoid Complex Processing**:\n  - AI recognition of formulas, tables, and PDF restructuring faces severe performance bottlenecks\n  - Complex AI processing leads to high computational costs\n  - Significantly increased processing time (potentially tens of seconds or more)\n  - Difficult to deploy at scale with low costs in production environments\n  - Not suitable for online services requiring quick response times\n\n- **Project Scope**:\n  - This project only serves to demonstrate the correct approach for layout-preserved PDF translation and AI-assisted PDF reading. Converting PDF files to markdown format for large language models to read, in my opinion, is not a wise approach.\n  - Aims for optimal performance-to-cost ratio\n\n- **Performance**:\n  - PolyglotPDF API response time: ~1 second per page\n  - Low computational resource requirements, suitable for scale deployment\n  - High cost-effectiveness for commercial applications\n\n- * Contact author:\nQQ： 1421243966\nemail: 1421243966@qq.com\n\nRelated questions answered and discussed：\n\n QQ group:\n 1031477425\n\n"
  },
  {
    "path": "README_CN.md",
    "content": "注： 对于pdf这种棘手的文件处理，对于文字版pdf的最优解：参考开源项目mupdf重构block识别算法只需要达到Adobe Acrobat Dc精度即可，不要舍近求远使用ocr扫描文字版pdf。 使用ai模型去理解pdf布局未来成本绝对会高于使用gpt4o mini这类价格！ 对于pdf种公式识别出要么不处理，要么通过字体文件名称和对应unicode值进行映射。 ocr扫描文字版pdf相当愚蠢\n# PolyglotPDF\n\n[![Python](https://img.shields.io/badge/python-3.8-blue.svg)](https://www.python.org/)\n[![PDF](https://img.shields.io/badge/pdf-documentation-brightgreen.svg)](https://example.com)\n[![LaTeX](https://img.shields.io/badge/latex-typesetting-orange.svg)](https://www.latex-project.org/)\n[![Translation](https://img.shields.io/badge/translation-supported-yellow.svg)](https://example.com)\n[![Math](https://img.shields.io/badge/math-formulas-red.svg)](https://example.com)\n[![PyMuPDF](https://img.shields.io/badge/PyMuPDF-1.24.0-blue.svg)](https://pymupdf.readthedocs.io/)\n\n## Demo\n<img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/demo.gif?raw=true\" width=\"80%\" height=\"40%\">\n\n### [🎬 Watch Full Video](https://github.com/CBIhalsen/PolyglotPDF/blob/main/demo.mp4)\n已经加入llms作为翻译api的选择，建议选择：Doubao ,Qwen ,deepseek v3 ,gpt4-o-mini。色彩空间错误可以通过填充PDF文件中的白色区域来解决。 古老text to text翻译api已删除\n\n另外，考虑添加arxiv搜索功能及对arxiv论文进行latex翻译后渲染。\n\n### 页面展示\n<div style=\"display: flex; margin-bottom: 20px;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page1.png?raw=true\" width=\"40%\" height=\"20%\" style=\"margin-right: 20px;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page2.jpeg?raw=true\" width=\"40%\" height=\"20%\">\n</div>\n<div style=\"display: flex;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page3.png?raw=true\" width=\"40%\" height=\"20%\" style=\"margin-right: 20px;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page4.png?raw=true\" width=\"40%\" height=\"20%\">\n</div>\n\n# 大语言模型API申请\n\n## 302.AI\nAI服务聚合平台，支持多种国际主流AI模型:\n- 官网地址: [302.AI](https://302.ai)\n- 注册地址: [邀请链接注册](https://share.302.ai/JBmCb1) (使用邀请码 `JBmCb1` 注册即送$1)\n- 支持模型: GPT-4o、GPT-4o-mini、Claude-3.7-Sonnet、DeepSeek-V3等多种模型\n- 特点: 一个账户即可使用多种AI模型，按使用量付费\n\n## Doubao & Deepseek\n通过火山引擎平台申请:\n- 申请地址: [火山引擎-豆包](https://www.volcengine.com/product/doubao/)\n- 支持模型: 豆包(Doubao)、Deepseek系列模型\n\n## 通义千问(Qwen)\n通过阿里云平台申请:\n- 申请地址: [阿里云-通义千问](https://cn.aliyun.com/product/tongyi?from_alibabacloud=&utm_content=se_1019997984) \n- 支持模型: Qwen-Max、Qwen-Plus等系列模型\n\n\n## 概述\nPolyglotPDF 是一款先进的 PDF 处理工具，采用特殊技术实现对 PDF 文档中的文字、表格和公式的超快速识别，通常仅需 1 秒即可完成处理。它支持 OCR 功能和完美保留版面的翻译功能，整篇文档的翻译通常可在 10 秒内完成（具体速度取决于翻译 API 服务商）。\n\n## 主要特点\n- **超快识别**：在约 1 秒内完成对 PDF 中文字、表格和公式的处理\n- **保留版面翻译**：翻译过程中完整保持原文档的排版格式\n- **OCR 支持**：高效处理扫描版文档\n- **基于文本的 PDF**：不需要GPU\n- **快速翻译**：约 10 秒内完成整个 PDF 的翻译\n- **灵活的 API 集成**：可对接各种翻译服务提供商\n- **网页对比界面**：支持原文与译文的并排对比\n- **增强的 OCR 功能**：提供更准确的文本识别和处理能力\n- **支持离线翻译**：使用较小翻译模型\n\n## 安装和设置\n\n<details>\n  <summary>标准安装</summary>\n\n1. 克隆仓库：\n```bash\ngit clone https://github.com/CBIhalsen/Polyglotpdf.git\ncd polyglotpdf\n```\n\n2. 安装依赖包：\n```bash\npip install -r requirements.txt\n```\n3. 在config.json内配置API密钥，不建议使用alicloud翻译API.\n\n4. 运行应用：\n```bash\npython app.py\n```\n\n5. 访问网页界面：\n在浏览器中打开 `http://127.0.0.1:8000`\n</details>\n\n<details>\n  <summary>Docker 安装</summary>\n\n## 无持久化快速启动\n\n如果您想快速测试PolyglotPDF而不设置持久化目录：\n\n```bash\n# 先拉取镜像\ndocker pull 2207397265/polyglotpdf:latest\n\n# 不挂载卷的容器运行（容器删除后数据将丢失）\ndocker run -d -p 12226:12226 --name polyglotpdf 2207397265/polyglotpdf:latest\n```\n\n这是尝试PolyglotPDF最快的方式，但容器停止后，所有上传的PDF和配置更改都会丢失。\n\n## 持久化存储安装\n\n```bash\n# 创建必要目录\nmkdir -p config fonts static/original static/target static/merged_pdf\n\n# 创建配置文件\nnano config/config.json    # 或使用任何文本编辑器\n# 复制项目中的配置模板到该文件\n# 注意填写您的API密钥等配置信息\n\n# 设置权限\nchmod -R 755 config fonts static\n```\n\n## 快速启动\n\n使用以下命令拉取并运行 PolyglotPDF Docker 镜像：\n\n```bash\n# 拉取镜像\ndocker pull 2207397265/polyglotpdf:latest\n\n# 运行容器\ndocker run -d -p 12226:12226 --name polyglotpdf \\\n  -v ./config/config.json:/app/config.json \\\n  -v ./fonts:/app/fonts \\\n  -v ./static/original:/app/static/original \\\n  -v ./static/target:/app/static/target \\\n  -v ./static/merged_pdf:/app/static/merged_pdf \\\n  2207397265/polyglotpdf:latest\n```\n\n## 访问应用\n\n容器启动后，在浏览器中打开：\n```\nhttp://localhost:12226\n```\n\n## 使用 Docker Compose\n\n创建 `docker-compose.yml` 文件：\n\n```yaml\nversion: '3'\nservices:\n  polyglotpdf:\n    image: 2207397265/polyglotpdf:latest\n    ports:\n      - \"12226:12226\"\n    volumes:\n      - ./config/config.json:/app/config.json # 配置文件\n      - ./fonts:/app/fonts # 字体文件\n      - ./static/original:/app/static/original # 原始PDF\n      - ./static/target:/app/static/target # 翻译后PDF\n      - ./static/merged_pdf:/app/static/merged_pdf # 合并PDF\n    restart: unless-stopped\n```\n\n然后运行：\n\n```bash\ndocker-compose up -d\n```\n## 常用 Docker 命令\n\n```bash\n# 停止容器\ndocker stop polyglotpdf\n\n# 重启容器\ndocker restart polyglotpdf\n\n# 查看日志\ndocker logs polyglotpdf\n```\n</details>\n\n\n\n## 环境要求\n- Python 3.8+\n- deepl==1.17.0\n- Flask==2.0.1\n- Flask-Cors==5.0.0\n- langdetect==1.0.9\n- Pillow==10.2.0\n- PyMuPDF==1.24.0\n- pytesseract==0.3.10\n- requests==2.31.0\n- tiktoken==0.6.0\n- Werkzeug==2.0.1\n\n## 致谢\n本项目得益于 PyMuPDF 强大的 PDF 处理和版面保持功能。\n\n## 即将推出的改进\n- PDF 聊天功能\n- 学术 PDF 搜索集成\n- 进一步提升处理速度\n\n### 待修复问题\n- **问题描述**：应用重编辑时发生错误: `code=4: only Gray, RGB, and CMYK colorspaces supported`\n- **现象**：文本块应用编辑时遇到不支持的色彩空间\n- **当前解决方案**：遇到不支持的色彩空间时跳过该文本块\n- **待解决思路**：对于包含不支持色彩空间的页面，整页切换至OCR模式处理\n- **复现示例**：[查看不支持色彩空间的PDF样例](https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/colorspace_issue_sample.pdf)\n\n\n### TODO\n- □ **自定义术语库**：支持自定义术语库，设置prompt进行领域专业翻译\n- □ **AI重排功能**：把双栏的PDF转换成HTML博客的单栏线性阅读格式，便于移动端阅读\n- □ **多格式导出**：翻译结果可以导出为PDF、HTML、Markdown等格式\n- □ **多端同步**：电脑上翻译完，手机上也能看\n- □ **增强合并逻辑**：现版本默认合并逻辑把检测字体名字全部关闭，加上水平、垂直、x、y范围重叠全部合并\n\n\n### 字体优化\n当前在 `main.py` 的 `start` 函数中，文本插入使用了默认字体配置：\n```python\n# 当前配置\ncss=f\"* {{font-family:{get_font_by_language(self.target_language)};font-size:auto;color: #111111 ;font-weight:normal;}}\"\n```\n\n你可以通过以下方式优化字体显示：\n\n1. **修改默认字体配置**\n```python\n# 自定义字体样式\ncss=f\"\"\"* {{\n    font-family: {get_font_by_language(self.target_language)};\n    font-size: auto;\n    color: #111111;\n    font-weight: normal;\n    letter-spacing: 0.5px;  # 调整字间距\n    line-height: 1.5;      # 调整行高\n}}\"\"\"\n```\n\n2. **嵌入自定义字体**\n你可以通过以下步骤嵌入自定义字体：\n- 将字体文件（如.ttf，.otf）放置在项目的 `fonts` 目录下\n- 在CSS中使用 `@font-face` 声明自定义字体\n```python\ncss=f\"\"\"\n@font-face {{\n    font-family: 'CustomFont';\n    src: url('fonts/your-font.ttf') format('truetype');\n}}\n* {{\n    font-family: 'CustomFont', {get_font_by_language(self.target_language)};\n    font-size: auto;\n    font-weight: normal;\n}}\n\"\"\"\n```\n\n### 基本原理\n本项目采用与 Adobe Acrobat DC 编辑 PDF 类似的基本原理，基于 PyMuPDF 识别和处理 PDF 文本块：\n\n- **核心处理流程**：\n```python\n# 获取页面中的文本块\nblocks = page.get_text(\"dict\")[\"blocks\"]\n\n# 遍历处理每个文本块\nfor block in blocks:\n    if block.get(\"type\") == 0:  # 文本块\n        bbox = block[\"bbox\"]     # 获取文本块边界框\n        text = \"\"\n        font_info = None\n        # 收集文本和字体信息\n        for line in block[\"lines\"]:\n            for span in line[\"spans\"]:\n                text += span[\"text\"] + \" \"\n```\n这种方式直接处理 PDF 文本块，保持原有布局不变，实现高效的文本提取和修改。\n\n- **技术选择**：\n  - 使用 PyMuPDF 进行 PDF 解析和编辑\n  - 专注于文本处理，避免复杂化问题\n  - 不进行 AI 识别公式、表格或页面重组等复杂操作\n\n- **为什么避免复杂处理**：\n  - AI 识别公式、表格和重组 PDF 页面的方式存在严重的性能瓶颈\n  - 复杂的 AI 处理导致计算成本高昂\n  - 处理时间显著增加（可能需要数十秒甚至更长）\n  - 难以在生产环境中大规模低成本部署\n  - 不适合需要快速响应的在线服务\n\n- **项目定位**：\n  - 主要用于保留布局的 PDF 文件翻译\n  - 为 AI 辅助阅读 PDF 提供高效实现方式\n  - 追求最佳性能价格比\n\n- **性能表现**：\n  - PolyglotPDF API 服务响应时间：约 1 秒/页\n  - 低计算资源消耗，适合规模化部署\n  - 成本效益高，适合商业应用\n\n"
  },
  {
    "path": "README_JA.md",
    "content": "# PolyglotPDF\n\n[![Python](https://img.shields.io/badge/python-3.8-blue.svg)](https://www.python.org/)\n[![PDF](https://img.shields.io/badge/pdf-documentation-brightgreen.svg)](https://example.com)\n[![LaTeX](https://img.shields.io/badge/latex-typesetting-orange.svg)](https://www.latex-project.org/)\n[![Translation](https://img.shields.io/badge/translation-supported-yellow.svg)](https://example.com)\n[![Math](https://img.shields.io/badge/math-formulas-red.svg)](https://example.com)\n[![PyMuPDF](https://img.shields.io/badge/PyMuPDF-1.24.0-blue.svg)](https://pymupdf.readthedocs.io/)\n\n## デモ\n<img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/demo.gif?raw=true\" width=\"80%\" height=\"40%\">\n\n### [🎬 フルビデオを見る](https://github.com/CBIhalsen/PolyglotPDF/blob/main/demo.mp4)\n翻訳APIの選択肢としてLLMsが追加されました。推奨モデル：Doubao、Qwen、deepseek v3、gpt4-o-miniです。カラースペースエラーはPDFファイルの白色領域を埋めることで解決できます。古いtext to text翻訳APIは削除されました。\n\nまた、arXiv検索機能とarXiv論文のLaTeX翻訳後のレンダリングの追加を検討中です。\n\n### ページ表示\n<div style=\"display: flex; margin-bottom: 20px;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page1.png?raw=true\" width=\"40%\" height=\"20%\" style=\"margin-right: 20px;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page2.jpeg?raw=true\" width=\"40%\" height=\"20%\">\n</div>\n<div style=\"display: flex;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page3.png?raw=true\" width=\"40%\" height=\"20%\" style=\"margin-right: 20px;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page4.png?raw=true\" width=\"40%\" height=\"20%\">\n</div>\n\n# 大規模言語モデルAPIの申請\n\n## 302.AI\n複数の国際主流AIモデルをサポートするAIサービス統合プラットフォーム:\n- 公式サイト: [302.AI](https://302.ai)\n- 登録サイト: [招待リンクで登録](https://share.302.ai/JBmCb1) (招待コード`JBmCb1`を使用して登録すると$1のボーナス)\n- 対応モデル: GPT-4o、GPT-4o-mini、Claude-3.5-Sonnet、DeepSeek-V3など\n- 特徴: 一つのアカウントで複数のAIモデルを利用可能、従量課金制\n\n## Doubao & Deepseek\n火山エンジンプラットフォームから申請:\n- 申請先: [火山エンジン-Doubao](https://www.volcengine.com/product/doubao/)\n- 対応モデル: Doubao、Deepseekシリーズモデル\n\n## 通義千問(Qwen)\nアリババクラウドプラットフォームから申請:\n- 申請先: [アリババクラウド-通義千問](https://cn.aliyun.com/product/tongyi?from_alibabacloud=&utm_content=se_1019997984)\n- 対応モデル: Qwen-Max、Qwen-Plusなどのシリーズモデル\n\n## 概要\nPolyglotPDFは、特殊技術を用いてPDF文書内のテキスト、表、数式を超高速で認識する先進的なPDF処理ツールです。通常1秒以内で処理を完了し、OCR機能と完全なレイアウト保持翻訳機能をサポートしています。文書全体の翻訳は通常10秒以内で完了します（翻訳APIプロバイダーによって速度は異なります）。\n\n## 主な特徴\n- **超高速認識**：約1秒でPDF内のテキスト、表、数式の処理を完了\n- **レイアウト保持翻訳**：翻訳時に原文書の書式を完全に保持\n- **OCRサポート**：スキャン版文書の効率的な処理\n- **テキストベースPDF**：GPUは不要\n- **高速翻訳**：約10秒でPDF全体の翻訳を完了\n- **柔軟なAPI統合**：各種翻訳サービスプロバイダーと連携可能\n- **Webベース比較インターフェース**：原文と訳文の並列比較をサポート\n- **強化されたOCR機能**：より正確なテキスト認識と処理能力\n- **オフライン翻訳対応**：小規模翻訳モデルの使用\n\n## インストールとセットアップ\n\n<details>\n  <summary>標準インストール</summary>\n\n1. リポジトリのクローン：\n```bash\ngit clone https://github.com/CBIhalsen/Polyglotpdf.git\ncd polyglotpdf\n```\n\n2. 依存パッケージのインストール：\n```bash\npip install -r requirements.txt\n```\n\n3. config.json内でAPIキーを設定。alicloud翻訳APIの使用は推奨されません。\n\n4. アプリケーションの実行：\n```bash\npython app.py\n```\n\n5. Webインターフェースへのアクセス：\nブラウザで `http://127.0.0.1:8000` を開く\n</details>\n\n<details>\n  <summary>Docker 使用方法</summary>\n\n## 永続化なしの簡易起動\n\n永続化ディレクトリを設定せずにPolyglotPDFをすぐにテストしたい場合：\n\n```bash\n# まずイメージをプル\ndocker pull 2207397265/polyglotpdf:latest\n\n# ボリュームをマウントせずにコンテナを実行（コンテナ削除後にデータは失われます）\ndocker run -d -p 12226:12226 --name polyglotpdf 2207397265/polyglotpdf:latest\n```\n\nこれはPolyglotPDFを試す最速の方法ですが、コンテナ停止後はアップロードしたPDFと設定変更がすべて失われます。\n\n## 永続化ストレージでのインストール\n\n```bash\n# 必要なディレクトリを作成\nmkdir -p config fonts static/original static/target static/merged_pdf\n\n# 設定ファイルを作成\nnano config/config.json    # または任意のテキストエディタを使用\n# プロジェクトの設定テンプレートをこのファイルにコピー\n# APIキーなどの設定情報を入力してください\n\n# 権限を設定\nchmod -R 755 config fonts static\n```\n\n## クイックスタート\n\n以下のコマンドでPolyglotPDF Dockerイメージをプルして実行：\n\n```bash\n# イメージをプル\ndocker pull 2207397265/polyglotpdf:latest\n\n# コンテナを実行\ndocker run -d -p 12226:12226 --name polyglotpdf \\\n  -v ./config/config.json:/app/config.json \\\n  -v ./fonts:/app/fonts \\\n  -v ./static/original:/app/static/original \\\n  -v ./static/target:/app/static/target \\\n  -v ./static/merged_pdf:/app/static/merged_pdf \\\n  2207397265/polyglotpdf:latest\n```\n\n## アプリケーションへのアクセス\n\nコンテナ起動後、ブラウザで開く：\n```\nhttp://localhost:12226\n```\n\n## Docker Composeの使用\n\n`docker-compose.yml`ファイルを作成：\n\n```yaml\nversion: '3'\nservices:\n  polyglotpdf:\n    image: 2207397265/polyglotpdf:latest\n    ports:\n      - \"12226:12226\"\n    volumes:\n      - ./config.json:/app/config.json # 設定ファイル\n      - ./fonts:/app/fonts # フォントファイル\n      - ./static/original:/app/static/original # 原本PDF\n      - ./static/target:/app/static/target # 翻訳後PDF\n      - ./static/merged_pdf:/app/static/merged_pdf # 結合PDF\n    restart: unless-stopped\n```\n\nそして実行：\n\n```bash\ndocker-compose up -d\n```\n\n## よく使うDockerコマンド\n\n```bash\n# コンテナを停止\ndocker stop polyglotpdf\n\n# コンテナを再起動\ndocker restart polyglotpdf\n\n# ログの確認\ndocker logs polyglotpdf\n```\n</details>\n\n## 環境要件\n- Python 3.8+\n- deepl==1.17.0\n- Flask==2.0.1\n- Flask-Cors==5.0.0\n- langdetect==1.0.9\n- Pillow==10.2.0\n- PyMuPDF==1.24.0\n- pytesseract==0.3.10\n- requests==2.31.0\n- tiktoken==0.6.0\n- Werkzeug==2.0.1\n\n## 謝辞\n本プロジェクトはPyMuPDFの強力なPDF処理とレイアウト保持機能の恩恵を受けています。\n\n## 今後の改善予定\n- PDFチャット機能\n- 学術PDF検索の統合\n- 処理速度のさらなる向上\n\n### 修正待ちの問題\n- **問題の説明**：アプリケーション再編集時のエラー: `code=4: only Gray, RGB, and CMYK colorspaces supported`\n- **現象**：テキストブロックの編集時に非対応のカラースペースが発生\n- **現在の解決策**：非対応のカラースペースを含むテキストブロックをスキップ\n- **解決へのアプローチ**：非対応のカラースペースを含むページ全体をOCRモードで処理\n- **再現サンプル**：[非対応カラースペースのPDFサンプルを見る](https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/colorspace_issue_sample.pdf)\n\n### TODO\n- □ **カスタム用語集**: カスタム用語集をサポートし、特定分野の専門的な翻訳のためのプロンプト設定\n- □ **AI再配置機能**: 二段組みPDFをHTMLブログの一列リニア読書形式に変換し、モバイル端末での読書を容易にする\n- □ **複数形式エクスポート**: 翻訳結果をPDF、HTML、Markdown等の形式にエクスポート可能\n- □ **マルチデバイス同期**: コンピュータで翻訳完了後、スマートフォンでも閲覧可能\n- □ **強化されたマージロジック**: 現バージョンのデフォルトマージロジックではフォント名検出を完全に無効にし、水平・垂直・x・y範囲の重複をすべてマージする\n\n### フォントの最適化\n現在、`main.py`の`start`関数では、デフォルトのフォント設定でテキストを挿入しています：\n```python\n# 現在の設定\ncss=f\"* {{font-family:{get_font_by_language(self.target_language)};font-size:auto;color: #111111 ;font-weight:normal;}}\"\n```\n\nフォント表示は以下の方法で最適化できます：\n\n1. **デフォルトフォント設定の変更**\n```python\n# カスタムフォントスタイル\ncss=f\"\"\"* {{\n    font-family: {get_font_by_language(self.target_language)};\n    font-size: auto;\n    color: #111111;\n    font-weight: normal;\n    letter-spacing: 0.5px;  # 文字間隔の調整\n    line-height: 1.5;      # 行の高さの調整\n}}\"\"\"\n```\n\n2. **カスタムフォントの埋め込み**\n以下の手順でカスタムフォントを埋め込むことができます：\n- フォントファイル（.ttf、.otfなど）をプロジェクトの`fonts`ディレクトリに配置\n- CSSで`@font-face`を使用してカスタムフォントを宣言\n```python\ncss=f\"\"\"\n@font-face {{\n    font-family: 'CustomFont';\n    src: url('fonts/your-font.ttf') format('truetype');\n}}\n* {{\n    font-family: 'CustomFont', {get_font_by_language(self.target_language)};\n    font-size: auto;\n    font-weight: normal;\n}}\n\"\"\"\n```\n\n### 基本原理\n本プロジェクトはAdobe Acrobat DCのPDF編集と同様の基本原理を採用し、PyMuPDFを使用してPDFテキストブロックを認識・処理します：\n\n- **コア処理フロー**：\n```python\n# ページからテキストブロックを取得\nblocks = page.get_text(\"dict\")[\"blocks\"]\n\n# 各テキストブロックを処理\nfor block in blocks:\n    if block.get(\"type\") == 0:  # テキストブロック\n        bbox = block[\"bbox\"]     # テキストブロックの境界ボックスを取得\n        text = \"\"\n        font_info = None\n        # テキストとフォント情報の収集\n        for line in block[\"lines\"]:\n            for span in line[\"spans\"]:\n                text += span[\"text\"] + \" \"\n```\nこの方法でPDFテキストブロックを直接処理し、元のレイアウトを保持したまま、効率的なテキストの抽出と修正を実現します。\n\n- **技術選択**：\n  - PyMuPDFを使用してPDFの解析と編集を行う\n  - テキスト処理に特化し、問題の複雑化を避ける\n  - 数式、表、ページ再構成などの複雑なAI認識は行わない\n\n- **複雑な処理を避ける理由**：\n  - 数式、表、PDFページ再構成のAI認識には深刻なパフォーマンスのボトルネックが存在\n  - 複雑なAI処理は計算コストが高額\n  - 処理時間が大幅に増加（数十秒以上かかる可能性）\n  - 本番環境での大規模な低コスト展開が困難\n  - オンラインサービスの迅速なレスポンスに不適\n\n- **プロジェクトの位置づけ**：\n  - レイアウトを保持したPDFファイルの翻訳が主目的\n  - PDFのAI支援読書に効率的な実装方法を提供\n  - 最適なパフォーマンスとコスト比を追求\n\n- **パフォーマンス**：\n  - PolyglotPDF APIサービスのレスポンス時間：約1秒/ページ\n  - 低計算リソース消費で、スケーラブルな展開が可能\n  - コスト効率が高く、商用利用に適している\n"
  },
  {
    "path": "README_KO.md",
    "content": "# PolyglotPDF\n\n[![Python](https://img.shields.io/badge/python-3.8-blue.svg)](https://www.python.org/)\n[![PDF](https://img.shields.io/badge/pdf-documentation-brightgreen.svg)](https://example.com)\n[![LaTeX](https://img.shields.io/badge/latex-typesetting-orange.svg)](https://www.latex-project.org/)\n[![Translation](https://img.shields.io/badge/translation-supported-yellow.svg)](https://example.com)\n[![Math](https://img.shields.io/badge/math-formulas-red.svg)](https://example.com)\n[![PyMuPDF](https://img.shields.io/badge/PyMuPDF-1.24.0-blue.svg)](https://pymupdf.readthedocs.io/)\n\n## 데모\n<img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/demo.gif?raw=true\" width=\"80%\" height=\"40%\">\n\n### [🎬 전체 영상 보기](https://github.com/CBIhalsen/PolyglotPDF/blob/main/demo.mp4)\n번역 API 선택지로 LLMs가 추가되었습니다. 권장 모델: Doubao, Qwen, deepseek v3, gpt4-o-mini입니다. 색상 공간 오류는 PDF 파일의 흰색 영역을 채우는 것으로 해결할 수 있습니다. 기존 text to text 번역 API는 삭제되었습니다.\n\n또한, arXiv 검색 기능과 arXiv 논문의 LaTeX 번역 후 렌더링 추가를 고려 중입니다.\n\n### 페이지 표시\n<div style=\"display: flex; margin-bottom: 20px;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page1.png?raw=true\" width=\"40%\" height=\"20%\" style=\"margin-right: 20px;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page2.jpeg?raw=true\" width=\"40%\" height=\"20%\">\n</div>\n<div style=\"display: flex;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page3.png?raw=true\" width=\"40%\" height=\"20%\" style=\"margin-right: 20px;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page4.png?raw=true\" width=\"40%\" height=\"20%\">\n</div>\n\n# 대규모 언어 모델 API 신청\n\n## 302.AI\n여러 국제 주류 AI 모델을 지원하는 AI 서비스 통합 플랫폼:\n- 공식 웹사이트: [302.AI](https://302.ai)\n- 가입하기: [초대 링크로 가입](https://share.302.ai/JBmCb1) (초대 코드 `JBmCb1` 사용 시 $1 보너스)\n- 지원 모델: GPT-4o, GPT-4o-mini, Claude-3.5-Sonnet, DeepSeek-V3 등\n- 특징: 하나의 계정으로 여러 AI 모델 사용 가능, 사용량 기반 과금\n\n## Doubao & Deepseek\n화산 엔진 플랫폼을 통한 신청:\n- 신청 주소: [화산 엔진-Doubao](https://www.volcengine.com/product/doubao/)\n- 지원 모델: Doubao, Deepseek 시리즈 모델\n\n## 통의천문(Qwen)\n알리바바 클라우드 플랫폼을 통한 신청:\n- 신청 주소: [알리바바 클라우드-통의천문](https://cn.aliyun.com/product/tongyi?from_alibabacloud=&utm_content=se_1019997984)\n- 지원 모델: Qwen-Max, Qwen-Plus 등 시리즈 모델\n\n## 개요\nPolyglotPDF는 특수 기술을 사용하여 PDF 문서 내의 텍스트, 표, 수식을 초고속으로 인식하는 선진적인 PDF 처리 도구입니다. 보통 1초 이내에 처리를 완료하며, OCR 기능과 완벽한 레이아웃 유지 번역 기능을 지원합니다. 문서 전체의 번역은 보통 10초 이내에 완료됩니다(번역 API 제공업체에 따라 속도가 다릅니다).\n\n## 주요 특징\n- **초고속 인식**: 약 1초 내에 PDF 내의 텍스트, 표, 수식 처리 완료\n- **레이아웃 유지 번역**: 번역 시 원문서의 서식을 완벽하게 유지\n- **OCR 지원**: 스캔 버전 문서의 효율적인 처리\n- **텍스트 기반 PDF**: GPU 불필요\n- **고속 번역**: 약 10초 내에 PDF 전체 번역 완료\n- **유연한 API 통합**: 각종 번역 서비스 제공업체와 연동 가능\n- **웹 기반 비교 인터페이스**: 원문과 번역문의 병렬 비교 지원\n- **강화된 OCR 기능**: 더 정확한 텍스트 인식과 처리 능력\n- **오프라인 번역 지원**: 소규모 번역 모델 사용\n\n## 설치 및 설정\n\n<details>\n  <summary>표준 설치</summary>\n\n1. 저장소 클론:\n```bash\ngit clone https://github.com/CBIhalsen/Polyglotpdf.git\ncd polyglotpdf\n```\n\n2. 의존성 패키지 설치:\n```bash\npip install -r requirements.txt\n```\n\n3. config.json에서 API 키 설정. alicloud 번역 API 사용은 권장되지 않습니다.\n\n4. 애플리케이션 실행:\n```bash\npython app.py\n```\n\n5. 웹 인터페이스 접속:\n브라우저에서 `http://127.0.0.1:8000` 열기\n</details>\n\n<details>\n  <summary>Docker 사용 방법</summary>\n\n## 비지속성 빠른 시작\n\n영구 디렉토리 설정 없이 PolyglotPDF를 빠르게 테스트하려면:\n\n```bash\n# 먼저 이미지 가져오기\ndocker pull 2207397265/polyglotpdf:latest\n\n# 볼륨 마운트 없이 컨테이너 실행(컨테이너 삭제 시 데이터 손실)\ndocker run -d -p 12226:12226 --name polyglotpdf 2207397265/polyglotpdf:latest\n```\n\n이것은 PolyglotPDF를 시도하는 가장 빠른 방법이지만, 컨테이너가 중지되면 업로드된 모든 PDF와 구성 변경 사항이 손실됩니다.\n\n## 영구 저장소 설치\n\n```bash\n# 필요한 디렉토리 생성\nmkdir -p config fonts static/original static/target static/merged_pdf\n\n# 설정 파일 생성\nnano config/config.json    # 또는 원하는 텍스트 편집기 사용\n# 프로젝트의 설정 템플릿을 이 파일에 복사\n# API 키 등의 설정 정보를 입력하세요\n\n# 권한 설정\nchmod -R 755 config fonts static\n```\n\n## 빠른 시작\n\n다음 명령을 사용하여 PolyglotPDF Docker 이미지를 가져와 실행:\n\n```bash\n# 이미지 가져오기\ndocker pull 2207397265/polyglotpdf:latest\n\n# 컨테이너 실행\ndocker run -d -p 12226:12226 --name polyglotpdf \\\n  -v ./config/config.json:/app/config.json \\\n  -v ./fonts:/app/fonts \\\n  -v ./static/original:/app/static/original \\\n  -v ./static/target:/app/static/target \\\n  -v ./static/merged_pdf:/app/static/merged_pdf \\\n  2207397265/polyglotpdf:latest\n```\n\n## 애플리케이션 접속\n\n컨테이너가 시작된 후, 브라우저에서 열기:\n```\nhttp://localhost:12226\n```\n\n## Docker Compose 사용\n\n`docker-compose.yml` 파일 생성:\n\n```yaml\nversion: '3'\nservices:\n  polyglotpdf:\n    image: 2207397265/polyglotpdf:latest\n    ports:\n      - \"12226:12226\"\n    volumes:\n      - ./config.json:/app/config.json # 설정 파일\n      - ./fonts:/app/fonts # 폰트 파일\n      - ./static/original:/app/static/original # 원본 PDF\n      - ./static/target:/app/static/target # 번역된 PDF\n      - ./static/merged_pdf:/app/static/merged_pdf # 병합된 PDF\n    restart: unless-stopped\n```\n\n그리고 실행:\n\n```bash\ndocker-compose up -d\n```\n\n## 자주 사용하는 Docker 명령어\n\n```bash\n# 컨테이너 중지\ndocker stop polyglotpdf\n\n# 컨테이너 재시작\ndocker restart polyglotpdf\n\n# 로그 확인\ndocker logs polyglotpdf\n```\n</details>\n\n## 환경 요구사항\n- Python 3.8+\n- deepl==1.17.0\n- Flask==2.0.1\n- Flask-Cors==5.0.0\n- langdetect==1.0.9\n- Pillow==10.2.0\n- PyMuPDF==1.24.0\n- pytesseract==0.3.10\n- requests==2.31.0\n- tiktoken==0.6.0\n- Werkzeug==2.0.1\n\n## 감사의 말\n본 프로젝트는 PyMuPDF의 강력한 PDF 처리와 레이아웃 유지 기능의 혜택을 받았습니다.\n\n## 향후 개선 예정\n- PDF 채팅 기능\n- 학술 PDF 검색 통합\n- 처리 속도 추가 향상\n\n### 수정 대기 중인 문제\n- **문제 설명**: 애플리케이션 재편집 시 오류: `code=4: only Gray, RGB, and CMYK colorspaces supported`\n- **현상**: 텍스트 블록 편집 시 지원되지 않는 색상 공간 발생\n- **현재 해결책**: 지원되지 않는 색상 공간을 포함한 텍스트 블록 건너뛰기\n- **해결 접근 방식**: 지원되지 않는 색상 공간을 포함한 페이지 전체를 OCR 모드로 처리\n- **재현 샘플**: [지원되지 않는 색상 공간의 PDF 샘플 보기](https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/colorspace_issue_sample.pdf)\n\n### TODO\n- □ **사용자 정의 용어집**: 사용자 정의 용어집을 지원하고, 특정 분야의 전문적인 번역을 위한 프롬프트 설정\n- □ **AI 재배치 기능**: 두 칸 PDF를 HTML 블로그의 한 줄 선형 읽기 형식으로 변환하여 모바일 장치에서 읽기 편하게 함\n- □ **다중 형식 내보내기**: 번역 결과를 PDF, HTML, Markdown 등 다양한 형식으로 내보내기\n- □ **다중 기기 동기화**: 컴퓨터에서 번역 완료한 후 모바일에서도 볼 수 있음\n- □ **향상된 병합 로직**: 현재 버전의 기본 병합 로직에서 글꼴 이름 감지를 모두 비활성화하고, 가로, 세로, x, y 범위 중복이 모두 병합되도록 함\n\n### 폰트 최적화\n현재 `main.py`의 `start` 함수에서는 기본 폰트 설정으로 텍스트를 삽입합니다:\n```python\n# 현재 설정\ncss=f\"* {{font-family:{get_font_by_language(self.target_language)};font-size:auto;color: #111111 ;font-weight:normal;}}\"\n```\n\n폰트 표시는 다음 방법으로 최적화할 수 있습니다:\n\n1. **기본 폰트 설정 변경**\n```python\n# 사용자 정의 폰트 스타일\ncss=f\"\"\"* {{\n    font-family: {get_font_by_language(self.target_language)};\n    font-size: auto;\n    color: #111111;\n    font-weight: normal;\n    letter-spacing: 0.5px;  # 자간 조정\n    line-height: 1.5;      # 행간 조정\n}}\"\"\"\n```\n\n2. **사용자 정의 폰트 임베딩**\n다음 단계로 사용자 정의 폰트를 임베딩할 수 있습니다:\n- 폰트 파일(.ttf, .otf 등)을 프로젝트의 `fonts` 디렉토리에 배치\n- CSS에서 `@font-face`를 사용하여 사용자 정의 폰트 선언\n```python\ncss=f\"\"\"\n@font-face {{\n    font-family: 'CustomFont';\n    src: url('fonts/your-font.ttf') format('truetype');\n}}\n* {{\n    font-family: 'CustomFont', {get_font_by_language(self.target_language)};\n    font-size: auto;\n    font-weight: normal;\n}}\n\"\"\"\n```\n\n### 기본 원리\n본 프로젝트는 Adobe Acrobat DC의 PDF 편집과 유사한 기본 원리를 채택하고, PyMuPDF를 사용하여 PDF 텍스트 블록을 인식하고 처리합니다:\n\n- **핵심 처리 흐름**:\n```python\n# 페이지에서 텍스트 블록 가져오기\nblocks = page.get_text(\"dict\")[\"blocks\"]\n\n# 각 텍스트 블록 처리\nfor block in blocks:\n    if block.get(\"type\") == 0:  # 텍스트 블록\n        bbox = block[\"bbox\"]     # 텍스트 블록의 경계 상자 가져오기\n        text = \"\"\n        font_info = None\n        # 텍스트와 폰트 정보 수집\n        for line in block[\"lines\"]:\n            for span in line[\"spans\"]:\n                text += span[\"text\"] + \" \"\n```\n이 방법으로 PDF 텍스트 블록을 직접 처리하여 원래 레이아웃을 유지한 채 효율적인 텍스트 추출과 수정을 실현합니다.\n\n- **기술 선택**:\n  - PyMuPDF를 사용하여 PDF 분석과 편집 수행\n  - 텍스트 처리에 특화하여 문제의 복잡화 방지\n  - 수식, 표, 페이지 재구성 등의 복잡한 AI 인식은 수행하지 않음\n\n- **복잡한 처리를 피하는 이유**:\n  - 수식, 표, PDF 페이지 재구성의 AI 인식에는 심각한 성능 병목 현상 존재\n  - 복잡한 AI 처리는 계산 비용이 높음\n  - 처리 시간이 크게 증가(수십 초 이상 소요 가능)\n  - 프로덕션 환경에서의 대규모 저비용 배포가 어려움\n  - 온라인 서비스의 신속한 응답에 부적합\n\n- **프로젝트 위치**:\n  - 레이아웃을 유지한 PDF 파일의 번역이 주목적\n  - PDF의 AI 지원 읽기에 효율적인 구현 방법 제공\n  - 최적의 성능과 비용 비율 추구\n\n- **성능**:\n  - PolyglotPDF API 서비스의 응답 시간: 약 1초/페이지\n  - 낮은 계산 리소스 소비로 확장 가능한 배포 가능\n  - 비용 효율이 높아 상업적 사용에 적합\n"
  },
  {
    "path": "README_TW.md",
    "content": "# PolyglotPDF\n\n[![Python](https://img.shields.io/badge/python-3.8-blue.svg)](https://www.python.org/)\n[![PDF](https://img.shields.io/badge/pdf-documentation-brightgreen.svg)](https://example.com)\n[![LaTeX](https://img.shields.io/badge/latex-typesetting-orange.svg)](https://www.latex-project.org/)\n[![Translation](https://img.shields.io/badge/translation-supported-yellow.svg)](https://example.com)\n[![Math](https://img.shields.io/badge/math-formulas-red.svg)](https://example.com)\n[![PyMuPDF](https://img.shields.io/badge/PyMuPDF-1.24.0-blue.svg)](https://pymupdf.readthedocs.io/)\n\n## 演示\n<img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/demo.gif?raw=true\" width=\"80%\" height=\"40%\">\n\n### [🎬 觀看完整影片](https://github.com/CBIhalsen/PolyglotPDF/blob/main/demo.mp4)\n翻譯API選項已新增LLMs。推薦模型：Doubao、Qwen、deepseek v3、gpt4-o-mini。色彩空間錯誤可透過填充PDF檔案的白色區域來解決。舊有的text to text翻譯API已被移除。\n\n此外，我們正在考慮新增arXiv搜尋功能和arXiv論文的LaTeX翻譯後渲染功能。\n\n### 頁面展示\n<div style=\"display: flex; margin-bottom: 20px;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page1.png?raw=true\" width=\"40%\" height=\"20%\" style=\"margin-right: 20px;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page2.jpeg?raw=true\" width=\"40%\" height=\"20%\">\n</div>\n<div style=\"display: flex;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page3.png?raw=true\" width=\"40%\" height=\"20%\" style=\"margin-right: 20px;\">\n    <img src=\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/page4.png?raw=true\" width=\"40%\" height=\"20%\">\n</div>\n\n# 大型語言模型API申請\n\n## 302.AI\n支援多種國際主流AI模型的AI服務整合平台：\n- 官方網站：[302.AI](https://302.ai)\n- 註冊連結：[邀請連結註冊](https://share.302.ai/JBmCb1) (使用邀請碼 `JBmCb1` 註冊即贈$1)\n- 支援模型：GPT-4o、GPT-4o-mini、Claude-3.5-Sonnet、DeepSeek-V3等\n- 特點：一個帳戶即可使用多種AI模型，按使用量付費\n\n## Doubao & Deepseek\n從火山引擎平台申請：\n- 申請地址：[火山引擎-Doubao](https://www.volcengine.com/product/doubao/)\n- 支援模型：Doubao、Deepseek系列模型\n\n## 通義千問(Qwen)\n從阿里雲平台申請：\n- 申請地址：[阿里雲-通義千問](https://cn.aliyun.com/product/tongyi?from_alibabacloud=&utm_content=se_1019997984)\n- 支援模型：Qwen-Max、Qwen-Plus等系列模型\n\n## 概述\nPolyglotPDF是一款使用特殊技術，能夠超高速識別PDF文件中文字、表格、數學公式的先進PDF處理工具。通常能在1秒內完成處理，並支援OCR功能和完整的版面保持翻譯功能。整份文件的翻譯通常能在10秒內完成（速度依翻譯API提供商而異）。\n\n## 主要特點\n- **超高速識別**：約1秒內完成PDF中文字、表格、數學公式的處理\n- **版面保持翻譯**：翻譯時完整保持原文件的格式\n- **OCR支援**：高效處理掃描版文件\n- **文字基礎PDF**：無需GPU\n- **快速翻譯**：約10秒完成PDF整體翻譯\n- **靈活API整合**：可與各種翻譯服務提供商連接\n- **網頁基礎比較介面**：支援原文與譯文並列比較\n- **強化OCR功能**：更準確的文字識別和處理能力\n- **離線翻譯支援**：使用小型翻譯模型\n\n## 安裝與設定\n\n<details>\n  <summary>標準安裝</summary>\n\n1. 複製儲存庫：\n```bash\ngit clone https://github.com/CBIhalsen/Polyglotpdf.git\ncd polyglotpdf\n```\n\n2. 安裝相依套件：\n```bash\npip install -r requirements.txt\n```\n\n3. 在config.json中設定API金鑰。不建議使用alicloud翻譯API。\n\n4. 執行應用程式：\n```bash\npython app.py\n```\n\n5. 存取網頁介面：\n在瀏覽器中開啟 `http://127.0.0.1:8000`\n</details>\n\n<details>\n  <summary>Docker 使用說明</summary>\n\n## 無持久化快速啟動\n\n如果您想快速測試PolyglotPDF而不設置持久化目錄：\n\n```bash\n# 先拉取映像\ndocker pull 2207397265/polyglotpdf:latest\n\n# 不掛載卷的容器運行（容器刪除後數據將丟失）\ndocker run -d -p 12226:12226 --name polyglotpdf 2207397265/polyglotpdf:latest\n```\n\n這是嘗試PolyglotPDF最快的方式，但容器停止後，所有上傳的PDF和配置更改都會丟失。\n\n## 持久化存儲安裝\n\n```bash\n# 創建必要目錄\nmkdir -p config fonts static/original static/target static/merged_pdf\n\n# 創建配置文件\nnano config/config.json    # 或使用任何文本編輯器\n# 將項目中的配置模板複製到該文件\n# 請注意填寫您的API金鑰等配置信息\n\n# 設置權限\nchmod -R 755 config fonts static\n```\n\n## 快速啟動\n\n使用以下命令拉取並運行 PolyglotPDF Docker 映像：\n\n```bash\n# 拉取映像\ndocker pull 2207397265/polyglotpdf:latest\n\n# 運行容器\ndocker run -d -p 12226:12226 --name polyglotpdf \\\n  -v ./config/config.json:/app/config.json \\\n  -v ./fonts:/app/fonts \\\n  -v ./static/original:/app/static/original \\\n  -v ./static/target:/app/static/target \\\n  -v ./static/merged_pdf:/app/static/merged_pdf \\\n  2207397265/polyglotpdf:latest\n```\n\n## 訪問應用\n\n容器啟動後，在瀏覽器中打開：\n```\nhttp://localhost:12226\n```\n\n## 使用 Docker Compose\n\n創建 `docker-compose.yml` 文件：\n\n```yaml\nversion: '3'\nservices:\n  polyglotpdf:\n    image: 2207397265/polyglotpdf:latest\n    ports:\n      - \"12226:12226\"\n    volumes:\n      - ./config.json:/app/config.json # 配置文件\n      - ./fonts:/app/fonts # 字體文件\n      - ./static/original:/app/static/original # 原始PDF\n      - ./static/target:/app/static/target # 翻譯後PDF\n      - ./static/merged_pdf:/app/static/merged_pdf # 合併PDF\n    restart: unless-stopped\n```\n\n然後運行：\n\n```bash\ndocker-compose up -d\n```\n\n## 常用 Docker 命令\n\n```bash\n# 停止容器\ndocker stop polyglotpdf\n\n# 重啟容器\ndocker restart polyglotpdf\n\n# 查看日誌\ndocker logs polyglotpdf\n```\n</details>\n\n## 環境需求\n- Python 3.8+\n- deepl==1.17.0\n- Flask==2.0.1\n- Flask-Cors==5.0.0\n- langdetect==1.0.9\n- Pillow==10.2.0\n- PyMuPDF==1.24.0\n- pytesseract==0.3.10\n- requests==2.31.0\n- tiktoken==0.6.0\n- Werkzeug==2.0.1\n\n## 致謝\n本專案受益於PyMuPDF強大的PDF處理和版面保持功能。\n\n## 未來改進計劃\n- PDF聊天功能\n- 學術PDF搜尋整合\n- 進一步提升處理速度\n\n### 待修正問題\n- **問題描述**：應用程式重新編輯時的錯誤：`code=4: only Gray, RGB, and CMYK colorspaces supported`\n- **現象**：編輯文字區塊時出現不支援的色彩空間\n- **目前解決方案**：跳過包含不支援色彩空間的文字區塊\n- **解決方向**：使用OCR模式處理包含不支援色彩空間的整個頁面\n- **重現範例**：[查看不支援色彩空間的PDF範例](https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/colorspace_issue_sample.pdf)\n\n### TODO\n- □ **自定義術語庫**：支援自定義術語庫，設置prompt進行領域專業翻譯\n- □ **AI重排功能**：把雙欄的PDF轉換成HTML部落格的單欄線性閱讀格式，便於移動端閱讀\n- □ **多格式匯出**：翻譯結果可以匯出為PDF、HTML、Markdown等格式\n- □ **多端同步**：電腦上翻譯完，手機上也能看\n- □ **增強合併邏輯**：現版本預設合併邏輯把檢測字體名字全部關閉，加上水平、垂直、x、y範圍重疊全部合併\n\n### 字型最佳化\n目前在`main.py`的`start`函數中，使用預設字型設定插入文字：\n```python\n# 目前設定\ncss=f\"* {{font-family:{get_font_by_language(self.target_language)};font-size:auto;color: #111111 ;font-weight:normal;}}\"\n```\n\n字型顯示可透過以下方式最佳化：\n\n1. **修改預設字型設定**\n```python\n# 自訂字型樣式\ncss=f\"\"\"* {{\n    font-family: {get_font_by_language(self.target_language)};\n    font-size: auto;\n    color: #111111;\n    font-weight: normal;\n    letter-spacing: 0.5px;  # 調整字距\n    line-height: 1.5;      # 調整行高\n}}\"\"\"\n```\n\n2. **嵌入自訂字型**\n可透過以下步驟嵌入自訂字型：\n- 將字型檔案（.ttf、.otf等）放置在專案的`fonts`目錄中\n- 在CSS中使用`@font-face`宣告自訂字型\n```python\ncss=f\"\"\"\n@font-face {{\n    font-family: 'CustomFont';\n    src: url('fonts/your-font.ttf') format('truetype');\n}}\n* {{\n    font-family: 'CustomFont', {get_font_by_language(self.target_language)};\n    font-size: auto;\n    font-weight: normal;\n}}\n\"\"\"\n```\n\n### 基本原理\n本專案採用與Adobe Acrobat DC的PDF編輯類似的基本原理，使用PyMuPDF識別和處理PDF文字區塊：\n\n- **核心處理流程**：\n```python\n# 從頁面取得文字區塊\nblocks = page.get_text(\"dict\")[\"blocks\"]\n\n# 處理每個文字區塊\nfor block in blocks:\n    if block.get(\"type\") == 0:  # 文字區塊\n        bbox = block[\"bbox\"]     # 取得文字區塊的邊界框\n        text = \"\"\n        font_info = None\n        # 收集文字和字型資訊\n        for line in block[\"lines\"]:\n            for span in line[\"spans\"]:\n                text += span[\"text\"] + \" \"\n```\n這種方式直接處理PDF文字區塊，在保持原始版面的同時，實現高效的文字擷取和修改。\n\n- **技術選擇**：\n  - 使用PyMuPDF進行PDF解析和編輯\n  - 專注於文字處理，避免問題複雜化\n  - 不進行複雜的AI識別，如數學公式、表格、頁面重構\n\n- **避免複雜處理的原因**：\n  - 數學公式、表格、PDF頁面重構的AI識別存在嚴重的效能瓶頸\n  - 複雜的AI處理計算成本高昂\n  - 處理時間大幅增加（可能需要數十秒以上）\n  - 難以在生產環境中進行大規模低成本部署\n  - 不適合線上服務的快速回應\n\n- **專案定位**：\n  - 主要目的是保持版面的PDF檔案翻譯\n  - 提供PDF AI輔助閱讀的高效實現方式\n  - 追求最佳效能和成本比\n\n- **效能表現**：\n  - PolyglotPDF API服務回應時間：約1秒/頁\n  - 低計算資源消耗，可擴展部署\n  - 成本效益高，適合商業使用\n"
  },
  {
    "path": "Subset_Font.py",
    "content": "from fontTools.subset import Subsetter, Options\nfrom fontTools.ttLib import TTFont\nimport datetime\nimport os\nimport requests\n\n\ndef download_font_from_github(language, font_filename, target_path):\n    \"\"\"\n    从GitHub下载字体文件\n    \"\"\"\n\n    # 构建GitHub原始文件URL\n    github_base_url = \"https://raw.githubusercontent.com/CBIhalsen/PolyglotPDF-fonts/main\"\n    font_folder = f\"{language}_fonts\"\n    github_url = f\"{github_base_url}/{font_folder}/{font_filename}\"\n\n    try:\n        # 下载文件\n        response = requests.get(github_url)\n\n        # 检查是否存在（GitHub返回404表示文件不存在）\n        if response.status_code == 404:\n            print(\"\\n=== 字体文件未找到 ===\")\n            print(f\"在GitHub仓库中未找到所需的字体文件:\")\n            print(f\"- 语言: {language}\")\n            print(f\"- 字体文件: {font_filename}\")\n            print(f\"- 预期路径: {font_folder}/{font_filename}\")\n            print(\"\\n请通过以下步骤请求添加字体：\")\n            print(\"1. 访问: https://github.com/CBIhalsen/PolyglotPDF-fonts\")\n            print(\"2. 创建新的Issue\")\n            print(\"3. 标题: [Font Request] Add font for {language}\")\n            print(\"4. 内容:\")\n            print(f\"   - Language: {language}\")\n            print(f\"   - Font filename: {font_filename}\")\n            print(f\"   - Expected path: {font_folder}/{font_filename}\")\n            print(\"   - Additional details: (请描述使用场景和需求)\\n\")\n            return False\n\n        response.raise_for_status()  # 检查其他可能的错误\n\n        # 创建目标文件夹并保存文件\n        os.makedirs(os.path.dirname(target_path), exist_ok=True)\n        with open(target_path, 'wb') as f:\n            f.write(response.content)\n\n        print(f\"成功从GitHub下载字体文件到: {target_path}\")\n        return True\n\n    except requests.exceptions.RequestException as e:\n        if isinstance(e, requests.exceptions.ConnectionError):\n            print(f\"网络连接错误: 无法连接到GitHub。请检查您的网络连接。\")\n        elif isinstance(e, requests.exceptions.Timeout):\n            print(f\"请求超时: GitHub响应时间过长。\")\n        else:\n            print(f\"下载字体文件失败: {str(e)}\")\n        return False\n\n\ndef check_glyph_coverage(font, text):\n    \"\"\"\n    检查字体是否包含所需的所有字形\n    返回未找到的字符列表\n    \"\"\"\n    cmap = font.getBestCmap()\n    missing_chars = []\n\n    for char in text:\n        if ord(char) not in cmap:\n            missing_chars.append(char)\n\n    return missing_chars\n\n\ndef subset_font(in_font_path, out_font_path, text, language):\n    b = datetime.datetime.now()\n    \"\"\"\n    使用 fontTools 对 in_font_path 做子集化，\n    只保留 text 中出现的字符，输出到 out_font_path。\n    \"\"\"\n\n    # 检查输入字体文件是否存在\n    if not os.path.exists(in_font_path):\n        print(f\"输入字体文件不存在: {in_font_path}\")\n        print(\"尝试从GitHub下载字体文件...\")\n\n        # 获取原始字体文件名\n        font_filename = os.path.basename(in_font_path)\n\n        # 尝试下载字体\n        if not download_font_from_github(language, font_filename, in_font_path):\n            print(\"无法获取字体文件，子集化操作终止\")\n            return\n\n    # 确保输出文件夹存在\n    output_dir = os.path.dirname(out_font_path)\n    if output_dir and not os.path.exists(output_dir):\n        os.makedirs(output_dir)\n        print(f\"创建输出目录: {output_dir}\")\n\n    # 去重并排序要保留的字符\n    unique_chars = \"\".join(sorted(set(text)))\n\n    # 读取原字体\n    font = TTFont(in_font_path)\n\n    # 检查字形覆盖\n    missing_chars = check_glyph_coverage(font, unique_chars)\n    if missing_chars:\n        print(\"\\n=== 字形缺失警告 ===\")\n        print(f\"字体文件 {os.path.basename(in_font_path)} 中未找到以下字符:\")\n        print(\"\".join(missing_chars))\n        print(\"这些字符将使用 PyMuPDF 默认字体进行显示\")\n        print(\"==================\\n\")\n\n        # 从text中移除缺失的字符,只对有字形的字符进行子集化\n        for char in missing_chars:\n            unique_chars = unique_chars.replace(char, '')\n\n    # 配置子集化选项\n    options = Options()\n\n    # 创建子集器并指定要包含的字符\n    subsetter = Subsetter(options=options)\n    subsetter.populate(text=unique_chars)\n\n    # 对字体做子集化\n    subsetter.subset(font)\n\n    # 保存子集化后的 TTF\n    font.save(out_font_path)\n    print(f\"生成子集字体: {out_font_path} (仅包含所需字形)\")\n\n    e = datetime.datetime.now()\n    elapsed_time = (e - b).total_seconds()\n    print(f\"子集化运行时间: {elapsed_time} 秒\")\n"
  },
  {
    "path": "YouDao_translation.py",
    "content": "import uuid\nimport requests\nimport hashlib\nimport time\nimport json\n\n\ndef translate(texts,original_lang, target_lang):\n    \"\"\"\n    有道翻译API接口\n\n    参数:\n    texts: list, 要翻译的文本列表\n    target_lang: str, 目标语言代码\n    credentials: dict, 包含 app_key 和 app_secret 的字典\n\n    返回:\n    list: 翻译后的文本列表\n    \"\"\"\n    YOUDAO_URL = 'https://openapi.youdao.com/v2/api'\n\n    with open(\"config.json\", 'r', encoding='utf-8') as f:\n        config = json.load(f)\n\n    # 获取指定服务的认证信息\n    if target_lang == 'zh':\n        target_lang='zh-CHS'\n    service_name = \"youdao\"\n    credentials = config['translation_services'].get(service_name)\n    if not credentials:\n        raise ValueError(f\"Translation service '{service_name}' not found in config\")\n\n\n    def encrypt(sign_str):\n        hash_algorithm = hashlib.sha256()\n        hash_algorithm.update(sign_str.encode('utf-8'))\n        return hash_algorithm.hexdigest()\n\n    def truncate(q):\n        if q is None:\n            return None\n        size = len(q)\n        return q if size <= 20 else q[0:10] + str(size) + q[size - 10:size]\n\n    def do_request(data):\n        headers = {'Content-Type': 'application/x-www-form-urlencoded'}\n        return requests.post(YOUDAO_URL, data=data, headers=headers)\n\n    try:\n        # 确保输入文本为列表格式\n        if isinstance(texts, str):\n            texts = [texts]\n\n\n        # 准备请求数据\n        data = {\n            'from': original_lang,\n            'to': target_lang,\n            'signType': 'v3',\n            'curtime': str(int(time.time())),\n            'appKey': credentials['app_key'],\n            'q': texts,\n            'salt': str(uuid.uuid1()),\n            'vocabId': \"您的用户词表ID\"\n        }\n\n        # 生成签名\n        sign_str = (credentials['app_key'] +\n                    truncate(''.join(texts)) +\n                    data['salt'] +\n                    data['curtime'] +\n                    credentials['app_secret'])\n        data['sign'] = encrypt(sign_str)\n\n        # 发送请求\n        response = do_request(data)\n        response_data = json.loads(response.content.decode(\"utf-8\"))\n\n        # 提取翻译结果\n        translations = [result[\"translation\"] for result in response_data[\"translateResults\"]]\n        print(translations)\n        return translations\n\n    except Exception as e:\n        print(f\"翻译出错: {str(e)}\")\n        return None\n# 使用示例:\nif __name__ == '__main__':\n    # 认证信息\n\n\n    # 要翻译的文本\n    texts = [\"很久很久以前\", '待输入的文字\"2', \"待输入的文字3\"]\n    original_lang = 'auto'\n\n    # 目标语言\n    target_lang = 'zh'\n\n    # 调用翻译\n    results = translate(texts,original_lang='auto', target_lang=target_lang)\n    print(results,'ggg')\n\n    if results:\n        for original, translated in zip(texts, results):\n            print(f\"原文: {original}\")\n            print(f\"译文: {translated}\\n\")\n\n"
  },
  {
    "path": "app.py",
    "content": "from flask import Flask, request, send_file, jsonify, send_from_directory\r\nimport json\r\nfrom pathlib import Path\r\n\r\nfrom threading import Thread\r\nimport atexit\r\nimport os\r\nimport sys\r\nimport webbrowser\r\nfrom main import main_function\r\nimport load_config\r\nfrom flask_cors import CORS\r\nfrom concurrent.futures import ThreadPoolExecutor\r\nfrom threading import Lock\r\nfrom load_config import delete_entry, decrease_count, get_default_services, update_default_services\r\nfrom convert2pdf import convert_to_pdf\r\nfrom threading import Timer\r\nfrom socketserver import ThreadingMixIn\r\nfrom werkzeug.serving import BaseWSGIServer, make_server\r\n\r\n\r\nclass ThreadedWSGIServer(ThreadingMixIn, BaseWSGIServer):\r\n    \"\"\"\r\n    支持多线程处理的 WSGI Server。\r\n    通过混入 ThreadingMixIn，让每个请求在单独的线程中处理，\r\n    从而避免单请求长时间阻塞其它请求。\r\n    \"\"\"\r\n    # 不需要额外的方法或属性，继承自 ThreadingMixIn 和 BaseWSGIServer 即可。\r\n    pass\r\n\r\ndef get_app_data_dir():\r\n    \"\"\"获取应用数据目录，确保跨平台兼容性\"\"\"\r\n    if getattr(sys, 'frozen', False):\r\n        # 打包后的应用\r\n        if sys.platform == 'darwin':  # macOS\r\n            # 在macOS上使用用户的Application Support目录\r\n            app_data = os.path.join(os.path.expanduser('~/Library/Application Support'), 'EbookTranslation')\r\n        elif sys.platform == 'linux':  # Linux\r\n            # 在Linux上使用~/.local/share目录\r\n            app_data = os.path.join(os.path.expanduser('~/.local/share'), 'EbookTranslation')\r\n        else:  # Windows或其他\r\n            # 在Windows上使用应用程序所在目录\r\n            app_data = os.path.dirname(sys.executable)\r\n    else:\r\n        # 开发环境\r\n        app_data = os.path.dirname(os.path.abspath(__file__))\r\n\r\n    # 确保目录存在\r\n    os.makedirs(app_data, exist_ok=True)\r\n    return app_data\r\n\r\n\r\n# 在app.py开头附近添加这一行\r\nAPP_DATA_DIR = get_app_data_dir()\r\n\r\napp = Flask(__name__, static_folder=None)  # 禁用默认的静态文件处理\r\n\r\nCORS(app)\r\n\r\n# 获取当前文件目录\r\ncurrent_dir = Path(APP_DATA_DIR)\r\n\r\n# 设置上传文件目录\r\nUPLOAD_DIR = os.path.join(APP_DATA_DIR, \"static\", \"original\")\r\nTARGET_DIR = os.path.join(APP_DATA_DIR, \"static\", \"target\")\r\n\r\n# 确保目录存在\r\nos.makedirs(UPLOAD_DIR, exist_ok=True)\r\nos.makedirs(TARGET_DIR, exist_ok=True)\r\n\r\n# 静态文件配置\r\napp.static_folder = 'static'\r\n\r\n# 创建线程池\r\nexecutor = ThreadPoolExecutor(max_workers=13)\r\n\r\n# 创建锁用于保护文件访问\r\nfile_lock = Lock()\r\n\r\n\r\n@app.route('/static/<path:filename>')\r\ndef serve_static(filename):\r\n    \"\"\"提供静态文件访问，使用APP_DATA_DIR中的文件\"\"\"\r\n    try:\r\n        # 直接使用APP_DATA_DIR中的static目录\r\n        static_dir = os.path.join(APP_DATA_DIR, 'static')\r\n        print(f\"Trying to serve: {os.path.join(static_dir, filename)}\")  # 调试输出\r\n\r\n        if os.path.exists(os.path.join(static_dir, filename)):\r\n            return send_from_directory(static_dir, filename)\r\n        else:\r\n            print(f\"File not found: {os.path.join(static_dir, filename)}\")  # 调试输出\r\n            return f\"File not found: {filename}\", 404\r\n\r\n    except Exception as e:\r\n        print(f\"Error serving static file: {e}\")  # 调试输出\r\n        return str(e), 500\r\n\r\n\r\n@app.route('/')\r\ndef read_index():\r\n    return send_file(current_dir / \"index.html\")\r\n\r\n@app.route('/pdfviewer.html')\r\ndef read_pdfviewer():\r\n    # 获取 URL 参数\r\n    index = request.args.get('index')\r\n    # print(index,'打开')\r\n\r\n    # 现在你可以使用这些参数了\r\n    load_config.update_file_status(index=int(index), read=\"1\")\r\n    return send_file(current_dir / \"pdfviewer.html\")\r\n\r\n@app.route('/pdfviewer2.html')\r\ndef read_pdfviewer2():\r\n    # 获取 URL 参数\r\n    index = request.args.get('index')\r\n    # print(index,'打开')\r\n\r\n    # 现在你可以使用这些参数了\r\n    load_config.update_file_status(index=int(index), read=\"1\")\r\n    return send_file(current_dir / \"pdfviewer2.html\")\r\n\r\n\r\n@app.route('/upload/', methods=['POST'])\r\ndef upload_file():\r\n    try:\r\n        if 'file' not in request.files:\r\n            return jsonify({\r\n                \"success\": False,\r\n                \"message\": \"No file part\"\r\n            }), 400\r\n\r\n        file = request.files['file']\r\n        if file.filename == '':\r\n            return jsonify({\r\n                \"success\": False,\r\n                \"message\": \"No selected file\"\r\n            }), 400\r\n\r\n        # 直接使用原始文件名，不使用 secure_filename\r\n        filename = file.filename\r\n        file_extension = os.path.splitext(filename)[1].lower()\r\n        print(filename, '文件名')\r\n\r\n        # 使用APP_DATA_DIR构建上传目录路径\r\n        UPLOAD_DIR = os.path.join(APP_DATA_DIR, 'static', 'original')\r\n        # 确保目录存在\r\n        os.makedirs(UPLOAD_DIR, exist_ok=True)\r\n\r\n        # 构建文件完整路径\r\n        file_path = os.path.join(UPLOAD_DIR, filename)\r\n\r\n        file.save(file_path)\r\n\r\n        # 如果不是PDF，进行转换\r\n        if file_extension != '.pdf':\r\n            # 创建PDF文件名\r\n            pdf_filename = os.path.splitext(filename)[0] + '.pdf'\r\n            pdf_file_path = os.path.join(UPLOAD_DIR, pdf_filename)\r\n\r\n            # 转换文件\r\n            if convert_to_pdf(input_file=file_path, output_file=pdf_file_path):\r\n                # 转换成功后删除原始文件\r\n                os.remove(file_path)\r\n\r\n                return jsonify({\r\n                    \"success\": True,\r\n                    \"message\": \"文件已成功转换为PDF并保存\",\r\n                    \"filename\": pdf_filename  # 返回PDF文件名\r\n                })\r\n            else:\r\n                # 转换失败，删除原始文件\r\n                os.remove(file_path)\r\n                return jsonify({\r\n                    \"success\": False,\r\n                    \"message\": \"PDF转换失败\"\r\n                }), 500\r\n        else:\r\n            # 如果是PDF文件，直接返回成功\r\n            return jsonify({\r\n                \"success\": True,\r\n                \"message\": \"PDF文件上传成功\",\r\n                \"filename\": filename\r\n            })\r\n\r\n    except Exception as e:\r\n        return jsonify({\r\n            \"success\": False,\r\n            \"message\": f\"上传失败: {str(e)}\"\r\n        }), 500\r\n\r\n@app.route('/translation', methods=['POST'])\r\ndef translate_files():\r\n    try:\r\n        data = request.get_json()\r\n        if not data:\r\n            return jsonify({\"error\": \"No JSON data provided\"}), 400\r\n\r\n        files = data.get('files', [])\r\n        if not files:\r\n            return jsonify({\"error\": \"No files provided\"}), 400\r\n\r\n        target_lang = data.get('targetLang', 'zh')\r\n        original_lang = data.get('sourceLang', 'en')  # 默认改为 'en'\r\n\r\n        print(f\"Processing files: {files}, target: {target_lang}, source: {original_lang}\")\r\n\r\n        # 修改翻译任务的函数调用方式\r\n        def translate_single_file(filename):\r\n            try:\r\n                # 直接使用 main_function 而不是 start\r\n                translator = main_function(\r\n                    original_language=original_lang,\r\n                    target_language=target_lang,\r\n                    pdf_path=filename\r\n                )\r\n                return translator.main()\r\n            except Exception as e:\r\n                print(f\"Error translating {filename}: {str(e)}\")\r\n                return {\"filename\": filename, \"error\": str(e)}\r\n\r\n        # 使用线程池并行处理翻译\r\n        futures = []\r\n        for filename in files:\r\n            # 分离文件名和扩展名\r\n            name, ext = os.path.splitext(filename)\r\n\r\n            # 如果扩展名不是 .pdf，则改为 .pdf\r\n            if ext.lower() != '.pdf':\r\n                filename = name + '.pdf'\r\n            print(f\"Submitting translation task for: {filename}\")\r\n            future = executor.submit(translate_single_file, filename)\r\n            futures.append(future)\r\n\r\n        # 等待所有翻译任务完成并收集结果\r\n        results = []\r\n        for future in futures:\r\n            try:\r\n                result = future.result()\r\n                results.append(result)\r\n            except Exception as e:\r\n                print(f\"Task execution error: {str(e)}\")\r\n                results.append({\"error\": str(e)})\r\n\r\n        return jsonify({\r\n            \"status\": \"success\",\r\n            \"message\": \"Translation tasks completed\",\r\n            \"results\": results\r\n        })\r\n\r\n    except Exception as e:\r\n        print(f\"Error in translate_files: {str(e)}\")\r\n        return jsonify({\r\n            \"status\": \"error\",\r\n            \"message\": f\"Failed to process translation request: {str(e)}\"\r\n        }), 500\r\n\r\n\r\n@app.route('/delete_article', methods=['POST'])\r\ndef delete_article():\r\n    \"\"\"\r\n    处理删除文章的请求\r\n\r\n    Returns:\r\n        Response: JSON响应，包含删除操作的结果\r\n    \"\"\"\r\n    try:\r\n        # 从请求中获取文章ID\r\n        data = request.get_json()\r\n        article_id = data.get('articleId')\r\n\r\n        if article_id is None:\r\n            return jsonify({'error': 'Missing article ID'}), 400\r\n\r\n        # 调用删除函数\r\n        print('删除', article_id)\r\n        success = delete_entry(int(article_id)) and decrease_count()\r\n        print('zzz', success)\r\n\r\n        if success:\r\n            return jsonify({'message': 'Article deleted successfully'}), 200\r\n        else:\r\n            return jsonify({'error': 'Failed to delete article'}), 500\r\n\r\n    except Exception as e:\r\n        print(f\"Error deleting article: {str(e)}\")\r\n        return jsonify({'error': 'Server error'}), 500\r\n\r\n\r\n@app.route('/delete_batch', methods=['POST'])\r\ndef delete_batch():\r\n    \"\"\"\r\n    处理批量删除文章的请求\r\n    :return: JSON 响应\r\n    \"\"\"\r\n    try:\r\n        data = request.get_json()\r\n        # 前端传来一个数组，例如 { \"articleIds\": [1,3,5] }\r\n        article_ids = data.get('articleIds', [])\r\n        if not article_ids:\r\n            return jsonify({'error': 'No article IDs provided'}), 400\r\n\r\n        # 用于统计删除成功/失败的数量\r\n        total = len(article_ids)\r\n        success_count = 0\r\n        failed_list = []\r\n\r\n        for article_id in article_ids:\r\n            try:\r\n                # 调用单条删除函数\r\n                success = delete_entry(int(article_id))\r\n                if success:\r\n                    # 如果删除成功也需要更新计数\r\n                    decrease_count()  # 假设你有一个 decrease_count() 函数\r\n                    success_count += 1\r\n                else:\r\n                    failed_list.append(article_id)\r\n            except Exception as e:\r\n                print(f\"Error deleting article {article_id}: {str(e)}\")\r\n                failed_list.append(article_id)\r\n\r\n        # 如果所有都成功，success_count == total\r\n        return jsonify({\r\n            'message': 'Batch delete attempted.',\r\n            'total': total,\r\n            'success_count': success_count,\r\n            'failed_list': failed_list\r\n        }), 200\r\n\r\n    except Exception as e:\r\n        print(f\"Error in batch delete: {str(e)}\")\r\n        return jsonify({'error': 'Server error'}), 500\r\n\r\n\r\n@app.route('/api/save-settings', methods=['POST'])\r\ndef save_settings():\r\n    try:\r\n        # 获取前端发送的JSON数据\r\n        data = request.get_json()\r\n\r\n        # 解析数据\r\n        translation_open = data.get('translation')\r\n        api_type = data.get('apiType')\r\n        ocr_value = data.get('OCR')\r\n\r\n        # 这里添加保存设置的逻辑\r\n        # 例如保存到数据库或配置文件\r\n        update_default_services(translation_open, api_type, ocr_value)  # 示例函数\r\n\r\n        # 返回成功响应\r\n        return jsonify({'message': '设置保存成功'}), 200\r\n\r\n    except Exception as e:\r\n        # 如果发生错误,返回错误响应\r\n        return jsonify({'error': str(e)}), 500\r\n\r\n\r\n@app.route('/api/get-default-services', methods=['GET'])\r\ndef get_default_services_route():\r\n    \"\"\"\r\n    获取默认服务配置的API路由\r\n\r\n    Returns:\r\n        JSON响应，包含默认服务配置信息或错误信息\r\n    \"\"\"\r\n    try:\r\n        services = get_default_services()\r\n\r\n        if services is None:\r\n            return jsonify({\r\n                'success': False,\r\n                'message': 'Failed to get default services configuration'\r\n            }), 400\r\n\r\n        return jsonify({\r\n            'success': True,\r\n            'data': services\r\n        })\r\n\r\n    except Exception as e:\r\n        return jsonify({\r\n            'success': False,\r\n            'message': f'Error: {str(e)}'\r\n        }), 500\r\n\r\n\r\n# 修改 get_recent() 函数\r\n@app.route('/recent.json')\r\ndef get_recent():\r\n    try:\r\n        data = load_config.load_recent()\r\n        if data is None:\r\n            return jsonify({})\r\n        return jsonify(data)\r\n    except Exception as e:\r\n        return jsonify({\"error\": str(e)}), 500\r\n\r\n# 修改 get_config() 函数\r\n@app.route('/config_json', methods=['GET'])\r\ndef get_config_json():\r\n    \"\"\"\r\n    前端页面首次加载时，会通过 GET /config_json 获取配置信息。\r\n    返回的内容就是前端需要渲染的 config.json 数据结构。\r\n    现在总是返回最新配置\r\n    \"\"\"\r\n    config_data = load_config.load_config(force_reload=True)\r\n    if config_data is None:\r\n        return jsonify({}), 500\r\n    return jsonify(config_data)\r\n\r\n# 修改 update_config() 函数\r\n@app.route('/update_config', methods=['POST'])\r\ndef update_config_json():\r\n    \"\"\"处理单个配置更新\"\"\"\r\n    try:\r\n        new_config = request.get_json()\r\n        if not new_config:\r\n            return jsonify({'status': 'error', 'message': '无效的配置数据'}), 400\r\n\r\n        # 加载当前配置\r\n        current_config = load_config.load_config()\r\n        if current_config is None:\r\n            return jsonify({'status': 'error', 'message': '无法加载当前配置'}), 500\r\n\r\n        # 递归更新配置\r\n        def update_dict(current, new):\r\n            for key, value in new.items():\r\n                if isinstance(value, dict) and key in current and isinstance(current[key], dict):\r\n                    update_dict(current[key], value)\r\n                else:\r\n                    current[key] = value\r\n\r\n        update_dict(current_config, new_config)\r\n\r\n        # 保存更新后的配置\r\n        if load_config.save_config(current_config):\r\n            return jsonify({'status': 'success', 'message': '配置已更新'}), 200\r\n        else:\r\n            return jsonify({'status': 'error', 'message': '保存配置失败'}), 500\r\n\r\n    except Exception as e:\r\n        return jsonify({'status': 'error', 'message': str(e)}), 500\r\n\r\n# 修改 save_all() 函数\r\n@app.route('/save_all', methods=['POST'])\r\ndef save_all():\r\n    \"\"\"完全覆盖保存所有配置\"\"\"\r\n    try:\r\n        new_config = request.get_json()\r\n        if not new_config:\r\n            return jsonify({'status': 'error', 'message': '无效的配置数据'}), 400\r\n\r\n        # 直接保存新配置\r\n        if load_config.save_config(new_config):\r\n            return jsonify({'status': 'success', 'message': '所有配置已保存'}), 200\r\n        else:\r\n            return jsonify({'status': 'error', 'message': '保存配置失败'}), 500\r\n\r\n    except Exception as e:\r\n        return jsonify({'status': 'error', 'message': str(e)}), 500\r\n\r\n\r\n@app.route('/api/config', methods=['GET'])\r\ndef get_config_api():\r\n    # 每次请求时强制重新加载配置\r\n    config_data = load_config.load_config(force_reload=True)\r\n    if config_data is None:\r\n        return jsonify({}), 500\r\n    return jsonify(config_data)\r\n\r\n@app.route('/api/config', methods=['POST'])\r\ndef update_config_api():\r\n    # 确保处理配置更新时能够正确处理grok相关的设置\r\n    # 通常这个函数应该直接将接收到的数据写入config.json，所以如果前端已正确发送grok配置，不需要修改这里\r\n    try:\r\n        new_config = request.get_json()\r\n        if not new_config:\r\n            return jsonify({'status': 'error', 'message': '无效的配置数据'}), 400\r\n\r\n        # 加载当前配置\r\n        current_config = load_config.load_config()\r\n        if current_config is None:\r\n            return jsonify({'status': 'error', 'message': '无法加载当前配置'}), 500\r\n\r\n        # 递归更新配置\r\n        def update_dict(current, new):\r\n            for key, value in new.items():\r\n                if isinstance(value, dict) and key in current and isinstance(current[key], dict):\r\n                    update_dict(current[key], value)\r\n                else:\r\n                    current[key] = value\r\n\r\n        update_dict(current_config, new_config)\r\n\r\n        # 保存更新后的配置\r\n        if load_config.save_config(current_config):\r\n            return jsonify({'status': 'success', 'message': '配置已更新'}), 200\r\n        else:\r\n            return jsonify({'status': 'error', 'message': '保存配置失败'}), 500\r\n\r\n    except Exception as e:\r\n        return jsonify({'status': 'error', 'message': str(e)}), 500\r\n\r\n\r\n@app.route(\"/translate_file\", methods=[\"POST\"])\r\ndef translate_file():\r\n    try:\r\n        # 翻译完成后确保更新状态\r\n        if translation_type == \"Grok\":\r\n            print(\"Grok translation completed, updating status\")\r\n        elif translation_type == \"ThirdParty\":\r\n            print(\"ThirdParty translation completed, updating status\")\r\n        elif translation_type == \"GLM\":\r\n            print(\"GLM translation completed, updating status\")\r\n        \r\n        # 更新翻译状态为已完成\r\n        update_translation_status(filename, '1')\r\n        return jsonify({\"status\": \"success\", \"message\": \"翻译完成\"})\r\n    except Exception as e:\r\n        print(f\"翻译过程中发生错误: {str(e)}\")\r\n        # 确保即使出错也更新状态，避免前端无限加载\r\n        update_translation_status(filename, '0')\r\n        return jsonify({\"status\": \"error\", \"message\": str(e)})\r\n\r\n# 辅助函数：更新翻译状态\r\ndef update_translation_status(filename, status):\r\n    \"\"\"更新指定文件的翻译状态\"\"\"\r\n    try:\r\n        with open(\"recent.json\", \"r\", encoding=\"utf-8\") as f:\r\n            data = json.load(f)\r\n        \r\n        for item in data:\r\n            if item.get(\"name\") == filename:\r\n                item[\"statue\"] = status\r\n                break\r\n        \r\n        with open(\"recent.json\", \"w\", encoding=\"utf-8\") as f:\r\n            json.dump(data, f, ensure_ascii=False, indent=2)\r\n        \r\n        print(f\"已更新文件 {filename} 的翻译状态为 {status}\")\r\n    except Exception as e:\r\n        print(f\"更新翻译状态时出错: {e}\")\r\n\r\n# 添加重新加载配置的路由\r\n@app.route('/api/reload-config', methods=['POST'])\r\ndef reload_config():\r\n    \"\"\"\r\n    强制重新加载配置文件\r\n    \r\n    Returns:\r\n        Response: JSON响应，包含重新加载的结果\r\n    \"\"\"\r\n    try:\r\n        # 强制重新加载配置\r\n        config = load_config.load_config(force_reload=True)\r\n        if config is None:\r\n            return jsonify({\r\n                'success': False,\r\n                'message': 'Failed to reload configuration'\r\n            }), 500\r\n            \r\n        return jsonify({\r\n            'success': True,\r\n            'message': 'Configuration reloaded successfully',\r\n            'config': config\r\n        })\r\n    except Exception as e:\r\n        return jsonify({\r\n            'success': False,\r\n            'message': f'Error reloading configuration: {str(e)}'\r\n        }), 500\r\n\r\nserver = None\r\n\r\nclass ServerThread(Thread):\r\n    \"\"\"后台运行的 Flask 服务器线程。\"\"\"\r\n    def __init__(self, flask_app, host=\"127.0.0.1\", port=12226):\r\n        super().__init__()\r\n        self.host = host\r\n        self.port = port\r\n        self.app = flask_app\r\n        self.srv = None\r\n\r\n    def run(self):\r\n        # 使用自定义的多线程 WSGI Server\r\n        self.srv = make_server(self.host, self.port, self.app, ThreadedWSGIServer)\r\n        self.srv.serve_forever()\r\n\r\n    def shutdown(self):\r\n        if self.srv:\r\n            self.srv.shutdown()\r\n\r\n\r\ndef open_browser():\r\n    webbrowser.open_new(\"http://127.0.0.1:12226\")\r\n# 修改主程序初始化部分\r\n\r\n\r\n@atexit.register\r\ndef on_exit():\r\n    \"\"\"Python 进程退出前，自动停止服务器。\"\"\"\r\n    print(\"程序退出，准备停止服务器...\")\r\n    global server\r\n    if server:\r\n        server.shutdown()\r\n        server.join()\r\n        print(\"服务器已停止。\")\r\nif __name__ == \"__main__\":\r\n    print(f\"Application data directory: {APP_DATA_DIR}\")\r\n    print(f\"Current directory: {current_dir}\")\r\n    print(\"Required files:\")\r\n    print(f\"- index.html: {'✓' if (current_dir / 'index.html').exists() else '✗'}\")\r\n\r\n    # 延迟打开浏览器\r\n    Timer(1, open_browser).start()\r\n\r\n    try:\r\n        # 创建并启动服务器\r\n        server = ServerThread(app, host=\"127.0.0.1\", port=12226)\r\n        server.daemon = True  # 设置为守护线程\r\n        server.start()\r\n        print(\"服务器已在 http://127.0.0.1:12226 运行...\")\r\n\r\n        # 保持主线程运行\r\n        while True:\r\n            try:\r\n                if not server.is_alive():\r\n                    break\r\n                server.join(1)  # 每秒检查一次服务器状态\r\n            except KeyboardInterrupt:\r\n                print(\"\\n接收到终止信号，正在关闭服务器...\")\r\n                server.shutdown()\r\n                break\r\n\r\n    except Exception as e:\r\n        print(f\"运行时错误: {e}\")\r\n    finally:\r\n        # 清理资源\r\n        executor.shutdown(wait=True)\r\n        if 'server' in locals() and server:\r\n            server.shutdown()\r\n"
  },
  {
    "path": "build.py",
    "content": "# import os\r\n# import sys\r\n# import platform\r\n# import subprocess\r\n# import shutil\r\n# from pathlib import Path\r\n#\r\n# def main():\r\n#     #────────────────────────────────────────────────────────────────────────\r\n#     # 1. 准备工作：获取当前目录、检查 PyInstaller\r\n#     #────────────────────────────────────────────────────────────────────────\r\n#     current_dir = Path(__file__).parent.absolute()\r\n#     print(f\"当前目录: {current_dir}\")\r\n#\r\n#     try:\r\n#         import PyInstaller\r\n#         print(\"PyInstaller 已安装\")\r\n#     except ImportError:\r\n#         print(\"安装 PyInstaller...\")\r\n#         subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"pyinstaller\"], check=True)\r\n#\r\n#     system = platform.system().lower()\r\n#     print(f\"当前系统: {system}\")\r\n#\r\n#     # 设置路径分隔符 (Windows 下为 ;，其他平台为 :)\r\n#     separator = ';' if system == 'windows' else ':'\r\n#\r\n#     # 生成可执行文件名称 (Windows 上会变成 EbookTranslator.exe，其它系统就没有后缀)\r\n#     exe_name = \"EbookTranslator\"\r\n#\r\n#     #────────────────────────────────────────────────────────────────────────\r\n#     # 2. 创建输出目录（供后面使用，onedir 模式下可自由放置打包产物）\r\n#     #────────────────────────────────────────────────────────────────────────\r\n#     dist_app_dir = current_dir / \"dist\" / exe_name\r\n#     os.makedirs(dist_app_dir, exist_ok=True)\r\n#\r\n#     #────────────────────────────────────────────────────────────────────────\r\n#     # 3. 根据你的需要，检查关键资源文件\r\n#     #────────────────────────────────────────────────────────────────────────\r\n#     required_files = {\r\n#         'app.py': True,\r\n#         'index.html': True,\r\n#         'config.json': True,\r\n#         'static': True,\r\n#         'recent.json': True\r\n#     }\r\n#     for file_name, required in required_files.items():\r\n#         file_path = current_dir / file_name\r\n#         if not file_path.exists() and required:\r\n#             print(f\"错误: 必要文件 '{file_name}' 不存在\")\r\n#             sys.exit(1)\r\n#\r\n#     #────────────────────────────────────────────────────────────────────────\r\n#     # 4. 构建 PyInstaller 的命令\r\n#     #    使用 --onedir 模式，并设置可执行文件名称为 EbookTranslator\r\n#     #────────────────────────────────────────────────────────────────────────\r\n#     pyinstaller_cmd = [\r\n#         sys.executable, '-m', 'PyInstaller',\r\n#         '--noconfirm',\r\n#         '--onedir',          # onedir 模式\r\n#         '--name', exe_name   # 生成文件(文件夹)名\r\n#     ]\r\n#\r\n#     # 如果在 Windows 平台，并且有 icon.ico，就使用图标\r\n#     icon_file = current_dir / \"icon.ico\"\r\n#     if system == 'windows' and icon_file.exists():\r\n#         pyinstaller_cmd.extend([\"--icon\", str(icon_file)])\r\n#\r\n#     #────────────────────────────────────────────────────────────────────────\r\n#     # 5. 设置 --add-data 参数，打包静态资源与需要的文件\r\n#     #────────────────────────────────────────────────────────────────────────\r\n#     data_files = []\r\n#     if (current_dir / 'static').exists():\r\n#         data_files.append((str(current_dir / 'static'), 'static'))\r\n#     if (current_dir / 'index.html').exists():\r\n#         data_files.append((str(current_dir / 'index.html'), '.'))\r\n#     if (current_dir / 'config.json').exists():\r\n#         data_files.append((str(current_dir / 'config.json'), '.'))\r\n#     if (current_dir / 'recent.json').exists():\r\n#         data_files.append((str(current_dir / 'recent.json'), '.'))\r\n#\r\n#     for src, dst in data_files:\r\n#         pyinstaller_cmd.extend(['--add-data', f\"{src}{separator}{dst}\"])\r\n#\r\n#     # 最后指定主脚本（app.py）\r\n#     pyinstaller_cmd.append(str(current_dir / 'app.py'))\r\n#\r\n#     #────────────────────────────────────────────────────────────────────────\r\n#     # 6. 打印并执行命令\r\n#     #────────────────────────────────────────────────────────────────────────\r\n#     print(\"执行 PyInstaller 命令:\\n\", \" \".join(map(str, pyinstaller_cmd)))\r\n#     try:\r\n#         subprocess.run(pyinstaller_cmd, check=True)\r\n#         print(\"PyInstaller 打包完成\")\r\n#     except Exception as e:\r\n#         print(f\"PyInstaller 打包失败: {e}\")\r\n#         sys.exit(1)\r\n#\r\n#     #────────────────────────────────────────────────────────────────────────\r\n#     # 7. 打包完成后，一般会在 dist/EbookTranslator 目录下看到:\r\n#     #    ├─ EbookTranslator.exe (Windows) 或 EbookTranslator(其它系统)\r\n#     #    ├─ 静态资源、依赖库、.. 等文件\r\n#     #────────────────────────────────────────────────────────────────────────\r\n#     build_dir = current_dir / \"build\"\r\n#     spec_file = current_dir / f\"{exe_name}.spec\"\r\n#\r\n#     # 清理临时文件\r\n#     if build_dir.exists():\r\n#         shutil.rmtree(build_dir)\r\n#     if spec_file.exists():\r\n#         spec_file.unlink()\r\n#\r\n#     print(\"Flask 应用打包完成！\\n请查看 dist/EbookTranslator 文件夹，\"\r\n#           \"其中的 EbookTranslator.exe(Windows) 或 EbookTranslator(其他平台) 即可运行。\")\r\n#\r\n#\r\n# if __name__ == \"__main__\":\r\n#     main()\r\n\r\n\r\nimport os\r\nimport sys\r\nimport platform\r\nimport subprocess\r\nimport shutil\r\nfrom pathlib import Path\r\n\r\n\r\ndef main():\r\n    # ────────────────────────────────────────────────────────────────────────\r\n    # 1. 准备工作：获取当前目录、检查 PyInstaller\r\n    # ────────────────────────────────────────────────────────────────────────\r\n    current_dir = Path(__file__).parent.absolute()\r\n    print(f\"当前目录: {current_dir}\")\r\n\r\n    try:\r\n        import PyInstaller\r\n        print(\"PyInstaller 已安装\")\r\n    except ImportError:\r\n        print(\"安装 PyInstaller...\")\r\n        subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"pyinstaller\"], check=True)\r\n\r\n    system = platform.system().lower()\r\n    print(f\"当前系统: {system}\")\r\n\r\n    # 生成可执行文件名称 (Windows 上会变成 EbookTranslator.exe，其它系统就没有后缀)\r\n    exe_name = \"EbookTranslator\"\r\n\r\n    # ────────────────────────────────────────────────────────────────────────\r\n    # 2. 创建输出目录\r\n    # ────────────────────────────────────────────────────────────────────────\r\n    dist_dir = current_dir / \"dist\"\r\n    dist_app_dir = dist_dir / exe_name\r\n\r\n    # 如果已存在，先删除\r\n    if dist_app_dir.exists():\r\n        print(f\"清理已存在的输出目录: {dist_app_dir}\")\r\n        shutil.rmtree(dist_app_dir)\r\n\r\n    os.makedirs(dist_app_dir, exist_ok=True)\r\n\r\n    # ────────────────────────────────────────────────────────────────────────\r\n    # 3. 检查关键资源文件\r\n    # ────────────────────────────────────────────────────────────────────────\r\n    required_files = {\r\n        'app.py': True,\r\n        'index.html': True,\r\n        'pdfviewer.html': True,\r\n        'pdfviewer2.html': True,  \r\n        'merge_pdf.py': True,     \r\n        'config.json': True,\r\n        'static': True\r\n    }\r\n    for file_name, required in required_files.items():\r\n        file_path = current_dir / file_name\r\n        if not file_path.exists() and required:\r\n            print(f\"错误: 必要文件 '{file_name}' 不存在\")\r\n            sys.exit(1)\r\n\r\n    # ────────────────────────────────────────────────────────────────────────\r\n    # 4. 构建 PyInstaller 的命令 - 不添加任何资源文件\r\n    # ────────────────────────────────────────────────────────────────────────\r\n    pyinstaller_cmd = [\r\n        sys.executable, '-m', 'PyInstaller',\r\n        '--noconfirm',\r\n        '--onedir',  # onedir 模式\r\n        '--name', exe_name,  # 生成文件(文件夹)名\r\n        ##'--windowed'  # 生成 macOS 的 .app 文件\r\n    ]\r\n\r\n    # 如果在 Windows 平台，并且有 icon.ico，就使用图标\r\n    icon_file = current_dir / \"icon.ico\"\r\n    if system == 'windows' and icon_file.exists():\r\n        pyinstaller_cmd.extend([\"--icon\", str(icon_file)])\r\n\r\n    # 最后指定主脚本（app.py）\r\n    pyinstaller_cmd.append(str(current_dir / 'app.py'))\r\n\r\n    # ────────────────────────────────────────────────────────────────────────\r\n    # 5. 执行 PyInstaller 命令\r\n    # ────────────────────────────────────────────────────────────────────────\r\n    print(\"执行 PyInstaller 命令:\\n\", \" \".join(map(str, pyinstaller_cmd)))\r\n    try:\r\n        subprocess.run(pyinstaller_cmd, check=True)\r\n        print(\"PyInstaller 打包完成\")\r\n    except Exception as e:\r\n        print(f\"PyInstaller 打包失败: {e}\")\r\n        sys.exit(1)\r\n\r\n    # ────────────────────────────────────────────────────────────────────────\r\n    # 6. 手动复制所有资源文件到输出目录\r\n    # ────────────────────────────────────────────────────────────────────────\r\n    print(\"\\n开始复制资源文件到输出目录...\")\r\n\r\n    # 复制 index.html\r\n    if (current_dir / 'index.html').exists():\r\n        print(f\"复制 index.html 到 {dist_app_dir}\")\r\n        shutil.copy2(current_dir / 'index.html', dist_app_dir / 'index.html')\r\n\r\n\r\n    if (current_dir / 'pdfviewer.html').exists():\r\n        print(f\"复制 pdfviewer.html 到 {dist_app_dir}\")\r\n        shutil.copy2(current_dir / 'pdfviewer.html', dist_app_dir / 'pdfviewer.html')\r\n        \r\n    # 复制 pdfviewer2.html \r\n    if (current_dir / 'pdfviewer2.html').exists():\r\n        print(f\"复制 pdfviewer2.html 到 {dist_app_dir}\")\r\n        shutil.copy2(current_dir / 'pdfviewer2.html', dist_app_dir / 'pdfviewer2.html')\r\n        \r\n    # 复制 merge_pdf.py \r\n    if (current_dir / 'merge_pdf.py').exists():\r\n        print(f\"复制 merge_pdf.py 到 {dist_app_dir}\")\r\n        shutil.copy2(current_dir / 'merge_pdf.py', dist_app_dir / 'merge_pdf.py')\r\n        \r\n    # 复制 config.json\r\n    if (current_dir / 'config.json').exists():\r\n        print(f\"复制 config.json 到 {dist_app_dir}\")\r\n        shutil.copy2(current_dir / 'config.json', dist_app_dir / 'config.json')\r\n\r\n    # 复制 recent.json (如果存在)\r\n    if (current_dir / 'recent.json').exists():\r\n        print(f\"复制 recent.json 到 {dist_app_dir}\")\r\n        shutil.copy2(current_dir / 'recent.json', dist_app_dir / 'recent.json')\r\n\r\n    # 复制 static 目录\r\n    if (current_dir / 'static').exists():\r\n        static_dest = dist_app_dir / 'static'\r\n        print(f\"复制 static 目录到 {static_dest}\")\r\n        if static_dest.exists():\r\n            shutil.rmtree(static_dest)\r\n        shutil.copytree(current_dir / 'static', static_dest)\r\n\r\n    # 复制其他可能需要的文件\r\n    other_files = ['README.md', 'LICENSE', 'requirements.txt']\r\n    for file_name in other_files:\r\n        if (current_dir / file_name).exists():\r\n            print(f\"复制 {file_name} 到 {dist_app_dir}\")\r\n            shutil.copy2(current_dir / file_name, dist_app_dir / file_name)\r\n\r\n    # ────────────────────────────────────────────────────────────────────────\r\n    # 7. 清理临时文件\r\n    # ────────────────────────────────────────────────────────────────────────\r\n    build_dir = current_dir / \"build\"\r\n    spec_file = current_dir / f\"{exe_name}.spec\"\r\n\r\n    if build_dir.exists():\r\n        print(f\"清理 build 目录: {build_dir}\")\r\n        shutil.rmtree(build_dir)\r\n    if spec_file.exists():\r\n        print(f\"删除 spec 文件: {spec_file}\")\r\n        spec_file.unlink()\r\n\r\n    # ────────────────────────────────────────────────────────────────────────\r\n    # 8. 完成\r\n    # ────────────────────────────────────────────────────────────────────────\r\n    print(\"\\n打包完成！\")\r\n    print(f\"应用程序位于: {dist_app_dir}\")\r\n    print(f\"可执行文件: {dist_app_dir / exe_name}{'.exe' if system == 'windows' else ''}\")\r\n    print(\"所有资源文件已直接复制到输出目录，可以直接查看和编辑。\")\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    main()"
  },
  {
    "path": "config.json",
    "content": "{\r\n  \"count\": 2,\r\n  \"PPC\": 20,\r\n  \"translation_services\": {\r\n    \"AI302\": {\r\n      \"auth_key\": \"\",\r\n      \"model_name\": \"\"\r\n    },\r\n    \"Doubao\": {\r\n      \"auth_key\": \"\",\r\n      \"model_name\": \"\"\r\n    },\r\n    \"GLM\": {\r\n      \"auth_key\": \"\",\r\n      \"model_name\": \"glm-4-flash\"\r\n    },\r\n    \"Grok\": {\r\n      \"auth_key\": \"\",\r\n      \"model_name\": \"grok-2-latest\"\r\n    },\r\n    \"Qwen\": {\r\n      \"auth_key\": \"\",\r\n      \"model_name\": \"qwen-plus\"\r\n    },\r\n    \"ThirdParty\": {\r\n      \"api_url\": \"\",\r\n      \"auth_key\": \"\",\r\n      \"model_name\": \"gpt-4o-mini\"\r\n    },\r\n    \"deepl\": {\r\n      \"auth_key\": \"\"\r\n    },\r\n    \"deepseek\": {\r\n      \"auth_key\": \"\",\r\n      \"model_name\": \"deepseek-chat\"\r\n    },\r\n    \"openai\": {\r\n      \"auth_key\": \"\",\r\n      \"model_name\": \"gpt-4o-mini\"\r\n    },\r\n    \"youdao\": {\r\n      \"app_key\": \"\",\r\n      \"app_secret\": \"\"\r\n    }\r\n  },\r\n  \"ocr_services\": {\r\n    \"tesseract\": {\r\n      \"path\": \"C:\\\\Program Files\\\\Tesseract-OCR\\\\tesseract.exe\"\r\n    }\r\n  },\r\n  \"default_services\": {\r\n    \"ocr_model\": false,\r\n    \"Enable_translation\": true,\r\n    \"Translation_api\": \"AI302\"\r\n  },\r\n  \"translation_prompt\": {\r\n    \"system_prompt\": \"You are a professional translator. Translate from {original_lang} to {target_lang}. Return only the translation without explanations or notes.\"\r\n  }\r\n}"
  },
  {
    "path": "convert2pdf.py",
    "content": "import fitz\nimport os\n\n\ndef convert_to_pdf(input_file, output_file=None):\n    \"\"\"\n    将支持的文档格式转换为 PDF，支持跨平台路径处理\n\n    Args:\n        input_file (str): 输入文件的完整路径\n        output_file (str, optional): 输出PDF文件的完整路径。如果为None，则使用输入文件名+.pdf\n\n    Returns:\n        bool: 转换是否成功\n    \"\"\"\n    try:\n        # 规范化路径，处理不同平台的路径分隔符\n        input_file = os.path.normpath(input_file)\n\n        if not os.path.exists(input_file):\n            print(f\"错误：输入文件 '{input_file}' 不存在\")\n            return False\n\n        # 如果未指定输出文件，则基于输入文件生成输出路径\n        if output_file is None:\n            # 获取文件名和目录\n            file_dir = os.path.dirname(input_file)\n            file_name = os.path.basename(input_file)\n            name_without_ext = os.path.splitext(file_name)[0]\n\n            # 在同一目录下创建同名PDF文件\n            output_file = os.path.join(file_dir, f\"{name_without_ext}.pdf\")\n\n        # 确保输出目录存在\n        output_dir = os.path.dirname(output_file)\n        if output_dir and not os.path.exists(output_dir):\n            os.makedirs(output_dir, exist_ok=True)\n\n        print(f\"正在处理文件: {input_file}\")\n        print(f\"输出文件将保存为: {output_file}\")\n\n        # 1. 先用 fitz.open 打开文档（EPUB、XPS、FB2 等格式）\n        doc = fitz.open(input_file)\n        print(f\"文档页数: {len(doc)}\")\n\n        # 2. 调用 convert_to_pdf() 得到 PDF 格式字节流\n        pdf_bytes = doc.convert_to_pdf()\n\n        # 3. 再以 \"pdf\" 格式打开这段字节流\n        pdf_doc = fitz.open(\"pdf\", pdf_bytes)\n\n        # 4. 保存为真正的 PDF 文件\n        pdf_doc.save(output_file)\n\n        # 关闭文档\n        pdf_doc.close()\n        doc.close()\n\n        # 检查输出文件是否成功创建\n        if os.path.exists(output_file):\n            print(f\"转换成功！PDF文件已保存为: {output_file}\")\n            return True\n        else:\n            print(\"转换似乎完成，但输出文件未找到\")\n            return False\n\n    except fitz.FileDataError as e:\n        print(f\"文件格式错误或文件损坏：{str(e)}\")\n    except PermissionError as e:\n        print(f\"权限错误：无法访问或写入文件 - {str(e)}\")\n    except Exception as e:\n        print(f\"转换失败，错误类型: {type(e).__name__}\")\n        print(f\"错误详情: {str(e)}\")\n        # 在调试模式下打印完整的堆栈跟踪\n        import traceback\n        traceback.print_exc()\n\n    return False\n# 使用示例\nif __name__ == \"__main__\":\n    # 单个文件转换示例\n    input_file = \"666 (1).epub\"\n\n    # 验证文件扩展名\n    if not input_file.lower().endswith(('.xps', '.epub', '.fb2', '.cbz', '.mobi')):\n        print(f\"不支持的文件格式。支持的格式包括: XPS, EPUB, FB2, CBZ, MOBI\")\n    else:\n        convert_to_pdf(input_file)\n\n    # 批量转换示例\n    # input_directory = \"documents\"\n    # batch_convert_to_pdf(input_directory)\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3'\nservices:\n  polyglotpdf:\n    image: 2207397265/polyglotpdf:latest\n    ports:\n      - \"12226:12226\"\n    volumes:\n      - ./config/config.json:/app/config.json # 配置文件\n      - ./fonts:/app/fonts # 字体文件\n      - ./static/original:/app/static/original # 原始PDF\n      - ./static/target:/app/static/target # 翻译后PDF\n      - ./static/merged_pdf:/app/static/merged_pdf # 合并PDF\n    restart: unless-stopped\n\n"
  },
  {
    "path": "download_model.py",
    "content": "import requests\nimport os\n\n\nsupport_language = [\n    \"en\",  # 英语 English\n    \"zh\",  # 中文 Chinese\n    \"es\",  # 西班牙语 Spanish\n    \"fr\",  # 法语 French\n    \"de\",  # 德语 German\n    \"ru\",  # 俄语 Russian\n    \"ar\",  # 阿拉伯语 Arabic\n    \"it\",  # 意大利语 Italian\n    \"ja\",  # 日语 Japanese\n    \"ko\",  # 韩语 Korean\n    \"nl\",  # 荷兰语 Dutch\n    \"pt\",  # 葡萄牙语 Portuguese\n    \"tr\",  # 土耳其语 Turkish\n    \"sv\",  # 瑞典语 Swedish\n    \"pl\",  # 波兰语 Polish\n    \"fi\",  # 芬兰语 Finnish\n    \"da\",  # 丹麦语 Danish\n    \"no\",  # 挪威语 Norwegian\n    \"cs\",  # 捷克语 Czech\n    \"el\",  # 希腊语 Greek\n    \"hu\",  # 匈牙利语 Hungarian\n    \"th\"   # 泰语 Thai\n]\n\ndef download_file(url, dest_folder, file_name):\n    \"\"\"\n    下载文件并保存到指定的文件夹中。\n    \"\"\"\n    response = requests.get(url, allow_redirects=True)\n    if response.status_code == 200:\n        with open(os.path.join(dest_folder, file_name), 'wb') as file:\n            file.write(response.content)\n    else:\n        print(f\"Failed to download {file_name}. Status code: {response.status_code}\")\n\ndef download_model_files(model_name):\n    \"\"\"\n    根据模型名称下载模型文件。\n    \"\"\"\n    # 文件列表\n    files_to_download = [\n        \"config.json\",\n        \"pytorch_model.bin\",\n        \"tokenizer_config.json\",\n        \"vocab.json\",\n        \"source.spm\",\n        \"target.spm\"  # 如果模型不使用SentencePiece，这两个文件可能不需要\n    ]\n\n    # 创建模型文件夹\n    # 创建模型文件夹\n    model_folder_name = model_name.split('/')[-1]  # 从模型名称中获取文件夹名称\n    model_folder = os.path.join(\"translation_models\", model_folder_name)  # 添加相对路径前缀\n\n    if os.path.exists(model_folder):\n        return\n\n\n    if not os.path.exists(model_folder):\n        os.makedirs(model_folder)\n\n    # 构建下载链接并下载文件\n    base_url = f\"https://huggingface.co/{model_name}/resolve/main/\"\n    for file_name in files_to_download:\n        download_url = base_url + file_name\n        print(f\"Downloading {file_name}...\")\n        download_file(download_url, model_folder, file_name)\n\n# 示例使用\nif __name__ == '__main__':\n\n    model_name = \"Helsinki-NLP/opus-mt-en-es\"\n    download_model_files(model_name)\n\n"
  },
  {
    "path": "get_new_blocks.py",
    "content": "#!/usr/bin/env python3\r\n# -*- coding: utf-8 -*-\r\n\r\nimport fitz  # PyMuPDF 的库名是 fitz\r\nimport math\r\nimport datetime\r\nimport re\r\nimport unicodedata\r\nfrom collections import defaultdict\r\n\r\nMATH_FONTS_SET = {\r\n    \"CMMI\", \"CMSY\", \"CMEX\", \"CMMI5\", \"CMMI6\", \"CMMI7\", \"CMMI8\", \"CMMI9\", \"CMMI10\",\r\n    \"CMSY5\", \"CMSY6\", \"CMSY7\", \"CMSY8\", \"CMSY9\", \"CMSY10\",\r\n    \"CMEX5\", \"CMEX6\", \"CMEX7\", \"CMEX8\", \"CMEX9\", \"CMEX10\",  # 新增CMEX字体家族\r\n    \"MSAM\", \"MSBM\", \"EUFM\", \"EUSM\", \"TXMI\", \"TXSY\", \"PXMI\", \"PXSY\",\r\n    \"CambriaMath\", \"AsanaMath\", \"STIXMath\", \"XitsMath\", \"Latin Modern Math\",\r\n    \"Neo Euler\", 'MTMI', 'MTSYN', 'TimesNewRomanPSMT'\r\n}\r\n\r\n\r\ndef snap_angle_func(raw_angle):\r\n    \"\"\"\r\n    将原始角度吸附到最接近的 0, 90, 180, 270, 360 这几个值，\r\n    并进行角度转换：270->90, 90->270, 360->0\r\n    \"\"\"\r\n    possible_angles = [0, 90, 180, 270, 360]\r\n    normalized_angle = raw_angle % 360\r\n    closest_angle = min(possible_angles, key=lambda x: abs(x - normalized_angle))\r\n    # 角度转换映射\r\n    angle_mapping = {\r\n        270: 90,\r\n        90: 270,\r\n        360: 0\r\n    }\r\n    return angle_mapping.get(closest_angle, closest_angle)\r\n\r\n\r\ndef horizontal_merge(\r\n    lines_data,\r\n    max_horizontal_gap=10,\r\n    max_y_diff=5,\r\n    check_font_size=False,\r\n    check_font_name=False,\r\n    check_font_color=False,\r\n    bold_max_horizontal_gap=20\r\n):\r\n    \"\"\"\r\n    水平方向合并（光标推进式），只对相邻两行递推判断能否合并，合并后光标不移动。\r\n    \"\"\"\r\n    if not lines_data:\r\n        return []\r\n\r\n    merged = []\r\n    i = 0\r\n    n = len(lines_data)\r\n    while i < n:\r\n        line = lines_data[i]\r\n        if not merged:\r\n            merged.append(line)\r\n            i += 1\r\n            continue\r\n\r\n        prev_line = merged[-1]\r\n        x0, y0, x1, y1 = line[\"line_bbox\"]\r\n        px0, py0, px1, py1 = prev_line[\"line_bbox\"]\r\n        curr_font_size = line[\"font_size\"] if line[\"font_size\"] else 10\r\n        prev_font_size = prev_line[\"font_size\"] if prev_line[\"font_size\"] else 10\r\n        avg_font_size = (curr_font_size+prev_font_size)/2\r\n        # (1) 如果 x、y 轴范围都有交集，就直接合并\r\n        overlap_y = (y0 <= py1) and (py0 <= y1) and (abs(y0-py1)>avg_font_size/5)\r\n        overlap_x = (x0 <= px1) and (px0 <= x1)\r\n        merged_this_round = False\r\n\r\n        if overlap_x and overlap_y:\r\n            prev_line[\"text\"] = prev_line[\"text\"].rstrip() + \" \" + line[\"text\"].lstrip()\r\n\r\n            new_x0 = min(px0, x0)\r\n            new_y0 = min(py0, y0)\r\n            new_x1 = max(px1, x1)\r\n            new_y1 = max(py1, y1)\r\n            prev_line[\"line_bbox\"] = (new_x0, new_y0, new_x1, new_y1)\r\n            prev_line[\"total_bold_chars\"] += line[\"total_bold_chars\"]\r\n            prev_line[\"total_nonbold_chars\"] += line[\"total_nonbold_chars\"]\r\n            prev_line[\"font_bold\"] = prev_line[\"total_bold_chars\"] > prev_line[\"total_nonbold_chars\"]\r\n            prev_line[\"font_names\"].extend(line[\"font_names\"])\r\n            prev_line[\"font_names\"] = list(set(prev_line[\"font_names\"]))\r\n            merged_this_round = True\r\n        else:\r\n            # (2) 细化合并条件\r\n            same_block = (line[\"block_index\"] == prev_line[\"block_index\"])\r\n            same_font_size_flag = (line[\"font_size\"] == prev_line[\"font_size\"])\r\n            same_font_name_flag = (line[\"font_name\"] == prev_line[\"font_name\"])\r\n\r\n            color_diff_val = 0\r\n            if line[\"font_color\"] is not None and prev_line[\"font_color\"] is not None:\r\n                color_diff_val = abs(line[\"font_color\"] - prev_line[\"font_color\"])\r\n            same_font_color_flag = (color_diff_val <= 50)\r\n\r\n            # 根据开关放宽判断\r\n            if not check_font_size:\r\n                same_font_size_flag = True\r\n            if not check_font_name:\r\n                same_font_name_flag = True\r\n            if not check_font_color:\r\n                same_font_color_flag = True\r\n\r\n\r\n\r\n            if line[\"font_bold\"] and prev_line[\"font_bold\"]:\r\n                effective_max_gap = avg_font_size\r\n            else:\r\n                effective_max_gap = avg_font_size\r\n\r\n            # y轴重叠判据\r\n\r\n            same_horizontal_line = abs(py1-y1)<avg_font_size/5   and abs(py0-y0)<avg_font_size/5\r\n\r\n            horizontal_gap = x0 - px1\r\n            close_enough = (0 <= horizontal_gap < effective_max_gap)\r\n\r\n            if (same_block and same_font_size_flag and same_font_name_flag\r\n                and same_font_color_flag and same_horizontal_line and close_enough\r\n            ):\r\n                prev_line[\"text\"] = (\r\n                    prev_line[\"text\"].rstrip() + \" \" + line[\"text\"].lstrip()\r\n                )\r\n\r\n                new_x0 = min(px0, x0)\r\n                new_y0 = min(py0, y0)\r\n                new_x1 = max(px1, x1)\r\n                new_y1 = max(py1, y1)\r\n                prev_line[\"line_bbox\"] = (new_x0, new_y0, new_x1, new_y1)\r\n                prev_line[\"total_bold_chars\"] += line[\"total_bold_chars\"]\r\n                prev_line[\"total_nonbold_chars\"] += line[\"total_nonbold_chars\"]\r\n                prev_line[\"font_bold\"] = prev_line[\"total_bold_chars\"] > prev_line[\"total_nonbold_chars\"]\r\n                prev_line[\"font_names\"].extend(line[\"font_names\"])\r\n                prev_line[\"font_names\"] = list(set(prev_line[\"font_names\"]))\r\n                merged_this_round = True\r\n\r\n        if merged_this_round:\r\n            # 合并成功，继续用当前 prev_line 和下一行比较\r\n            i += 1\r\n            continue\r\n        else:\r\n            # 没合并，推进光标，当前行直接进 merged\r\n            merged.append(line)\r\n            i += 1\r\n\r\n    # 打印合并后结果\r\n    # for idx, line in enumerate(merged, 1):\r\n    #     text = line[\"text\"]\r\n    #     bbox = line[\"line_bbox\"]\r\n    #     print(f\"水平行 {idx}: text = {text!r}, bbox = {bbox}\")\r\n\r\n    return merged\r\n\r\ndef merge_lines(lines_data, check_font_size=False, check_font_name=True, check_font_color=True,check_same_block=True ):\r\n    \"\"\"\r\n    垂直方向合并函数（合并后继续检查当前位置，不全量遍历回头）\r\n    支持四种合并逻辑 condition_1 ~ condition_4\r\n# condition_1: “中间合并豁免”或“包裹型合并（对称）”\r\n# 说明：当前行左右都被上一行包住，中间至少留 margin_in_middle 空隙，\r\n# 并且左右包裹“对称性好”（左右两端冗余的差值不能太大），\r\n# 常见于公式换行、PDF多栏切分等情形，防止“孤行”被误合并进大段。\r\ncondition_1 = (\r\n    same_block and same_font_size_flag and same_font_name_flag and same_font_color_flag\r\n    and y_distance_small\r\n    and (x0 >= px0 + margin_in_middle) and (x1 <= px1 - margin_in_middle)\r\n    and no_end_indent\r\n    and abs(abs(x0-px0) - abs(x1-px1)) < max_horizontal_gap\r\n)\r\n\r\n# condition_2: “新逻辑合并”\r\n# 说明：只要同块、竖直距离和水平距离都很近（左右对齐良好），并且没有末尾缩进，\r\n# 主要针对“强制换行”、“正文伪换行”等排版产生的断行，适合合并正文连续段落。\r\ncondition_2 = (\r\n    same_block and y_distance_small and x_distance_small\r\n    and (abs(px0 - x0) < margin_in_middle) and no_end_indent\r\n)\r\n\r\n# condition_3: “经典合并”或“宽度近似合并”\r\n# 说明：传统正文合并判据，要求同块、字体等属性接近、左右对齐好、宽度差值极小且无缩进。\r\n# 主要用于正常正文行的自然换行处理。\r\ncondition_3 = (\r\n    same_block and y_distance_small and same_font_size_flag and same_font_name_flag and same_font_color_flag\r\n    and x_distance_small and no_end_indent\r\n)\r\n\r\n# condition_4: “BBox包裹合并（防漏）”\r\n# 说明：上一行的bbox能完整包裹当前行（含一定容差 tolerance），常用于\r\n# 脚注、特殊符号、页眉、页脚等特殊情况防止“残留孤行”被遗漏。\r\ncondition_4 = (\r\n    same_block\r\n    and (x0 >= px0 - tolerance) and (y0 >= py0 - tolerance)\r\n    and (x1 <= px1 + tolerance) and (y1 <= py1 + tolerance)\r\n    and no_end_indent\r\n)\r\n\r\n# condition_5: “二级新逻辑合并/容忍性补充”\r\n# 说明：同块，竖直距离和水平距离都近，且左边界只比上一行多一点（不过多于2倍gap），\r\n# 用于捕捉有轻微缩进或特殊段落起始的伪断行，防止漏合并。\r\ncondition_5 = (\r\n    same_block and y_distance_small and x_distance_small\r\n    and (px0 - x0) < max_horizontal_gap * 2\r\n    and no_end_indent\r\n)\r\n\r\n\r\n    \"\"\"\r\n\r\n    merged = []\r\n    i = 0\r\n    n = len(lines_data)\r\n    while i < n:\r\n        line = lines_data[i]\r\n        if not merged:\r\n            merged.append(line)\r\n            i += 1\r\n            continue\r\n\r\n        prev_line = merged[-1]\r\n        x0, y0, x1, y1 = line[\"line_bbox\"]\r\n        px0, py0, px1, py1 = prev_line[\"line_bbox\"]\r\n        current_width = (x1 - x0)\r\n        prev_width = (px1 - px0)\r\n        prev_indent = prev_line['indent']\r\n\r\n        # 判断同一块，增加是否启用的开关\r\n        if check_same_block:\r\n            same_block = (line[\"block_index\"] == prev_line[\"block_index\"])\r\n        else:\r\n            same_block = True  # 不检查时总为True\r\n        # 无缩进\r\n        no_end_indent = prev_line[\"end_indent\"] == 0\r\n\r\n        # 字体大小布尔标记\r\n        if check_font_size:\r\n            if line[\"font_size\"] is not None and prev_line[\"font_size\"] is not None:\r\n                font_size_diff = abs(line[\"font_size\"] - prev_line[\"font_size\"])\r\n                same_font_size_flag = (font_size_diff <= 0.6)\r\n            else:\r\n                same_font_size_flag = True\r\n        else:\r\n            same_font_size_flag = True\r\n        # 字体名称布尔标记\r\n        if check_font_name:\r\n            same_font_name_flag = (line[\"font_name\"] == prev_line[\"font_name\"])\r\n        else:\r\n            same_font_name_flag = True\r\n        # 字体颜色布尔标记\r\n        color_diff_val = 0\r\n        if line[\"font_color\"] is not None and prev_line[\"font_color\"] is not None:\r\n            color_diff_val = abs(line[\"font_color\"] - prev_line[\"font_color\"])\r\n        if check_font_color:\r\n            same_font_color_flag = (color_diff_val <= 50)\r\n        else:\r\n            same_font_color_flag = True\r\n\r\n        # 动态调整阈值\r\n        curr_font_size = line[\"font_size\"] if line[\"font_size\"] else 10\r\n        prev_font_size = prev_line[\"font_size\"] if prev_line[\"font_size\"] else 10\r\n        max_horizontal_gap = (curr_font_size + prev_font_size) / 2.0\r\n        margin_in_middle = max_horizontal_gap / 1.5\r\n        max_x_distance = max_horizontal_gap * 8\r\n\r\n        # Y 轴、X 轴距离\r\n        y_distance = (y0 - py1)\r\n        y_distance_small = (abs(y_distance) < max_horizontal_gap/1.3)\r\n        horizontal_distance = abs(x0 - px0)\r\n        x_distance_small = (horizontal_distance < max_x_distance)\r\n\r\n        thorizontal_distance = abs(current_width - prev_width)\r\n\r\n\r\n        # 判断水平/竖直重叠\r\n\r\n        avg_font_size = (curr_font_size+prev_font_size)/2\r\n\r\n\r\n        overlap_y = (y0 <= py1) and (py0 <= y1) and (abs(y0-py1)>avg_font_size/5)\r\n        overlap_x = (x0 <= px1) and (px0 <= x1)\r\n\r\n        # 合并条件\r\n        # condition_1: “中间合并豁免”\r\n        condition_1 = (\r\n            same_block and same_font_size_flag and same_font_name_flag and same_font_color_flag\r\n            and y_distance_small and (x0 >= px0 + margin_in_middle) and (x1 <= px1 - margin_in_middle)\r\n            and no_end_indent and abs (abs(x0-px0) - abs(x1-px1)) < max_horizontal_gap\r\n        )\r\n        # condition_2: “新逻辑合并”\r\n        condition_2 = (\r\n            same_block and y_distance_small and x_distance_small\r\n            and (abs(px0 - x0) < margin_in_middle) and no_end_indent and ((current_width - prev_width) < max_horizontal_gap*2)\r\n        )\r\n        # condition_3: “老逻辑合并”\r\n        condition_3 = (\r\n            same_block and y_distance_small and same_font_size_flag and same_font_name_flag and same_font_color_flag\r\n            and x_distance_small and no_end_indent\r\n        )\r\n        # condition_4: “包裹合并”\r\n        tolerance = max_horizontal_gap / 2\r\n        condition_4 = (\r\n            same_block and (x0 >= px0 - tolerance) and (y0 >= py0 - tolerance)\r\n            and (x1 <= px1 + tolerance) and (y1 <= py1 + tolerance) and no_end_indent\r\n        )\r\n        # condition_5: “”\r\n        condition_5 = (\r\n            same_block and y_distance_small and x_distance_small\r\n            and (px0-x0)< max_horizontal_gap *2 and no_end_indent and(abs(current_width-prev_width)<max_x_distance)\r\n        )\r\n\r\n        merged_this_round = False\r\n\r\n        # 依次判断合并逻辑\r\n        if overlap_x and overlap_y:\r\n            # 水平合并\r\n            prev_line[\"text\"] = prev_line[\"text\"].rstrip() + \" \" + line[\"text\"].lstrip()\r\n\r\n            if prev_indent :\r\n                indent_val = prev_indent\r\n                # print('当前合并2发的行','上一行：', prev_line[\"text\"],'当前行:',line[\"text\"] )\r\n            else:\r\n                if (px0 > x0):\r\n                    indent_val = px0 - x0\r\n                else:\r\n                    indent_val = 0\r\n\r\n            end_indent_val = abs(px1 - x1) if (px1 > x1 and abs(px1 - x1) > max_horizontal_gap) else 0\r\n            merged[-1][\"end_indent\"] = end_indent_val\r\n\r\n            merged[-1][\"end_indent\"] = end_indent_val\r\n            new_x0 = min(px0, x0)\r\n            new_y0 = min(py0, y0)\r\n            new_x1 = max(px1, x1)\r\n            new_y1 = max(py1, y1)\r\n            prev_line[\"line_bbox\"] = (new_x0, new_y0, new_x1, new_y1)\r\n            prev_line[\"total_bold_chars\"] += line[\"total_bold_chars\"]\r\n            prev_line[\"total_nonbold_chars\"] += line[\"total_nonbold_chars\"]\r\n            prev_line[\"font_bold\"] = prev_line[\"total_bold_chars\"] > prev_line[\"total_nonbold_chars\"]\r\n            prev_line[\"font_names\"].extend(line[\"font_names\"])\r\n            prev_line[\"font_names\"] = list(set(prev_line[\"font_names\"]))\r\n            merged[-1][\"indent\"] = indent_val\r\n\r\n            # print(\"重叠合并文本：\", prev_line[\"text\"])\r\n            merged_this_round = True\r\n        elif condition_1:\r\n\r\n            merged[-1][\"text\"] = prev_line[\"text\"].rstrip() + \" \" + line[\"text\"].lstrip()\r\n\r\n            new_x0 = min(px0, x0)\r\n            new_y0 = min(py0, y0)\r\n            new_x1 = max(px1, x1)\r\n            new_y1 = max(py1, y1)\r\n            merged[-1][\"line_bbox\"] = (new_x0, new_y0, new_x1, new_y1)\r\n            merged[-1][\"total_bold_chars\"] += line[\"total_bold_chars\"]\r\n            merged[-1][\"total_nonbold_chars\"] += line[\"total_nonbold_chars\"]\r\n            merged[-1][\"font_bold\"] = merged[-1][\"total_bold_chars\"] > merged[-1][\"total_nonbold_chars\"]\r\n            merged[-1][\"font_names\"].extend(line[\"font_names\"])\r\n            merged[-1][\"font_names\"] = list(set(merged[-1][\"font_names\"]))\r\n            if line[\"font_size\"] is not None and line[\"font_size\"] > margin_in_middle * 2:\r\n                merged[-1][\"type\"] = \"title\"\r\n            # print('合并1', merged[-1][\"text\"])\r\n            merged_this_round = True\r\n        elif condition_2:\r\n            # 如果上一行已经有缩进，直接继承\r\n            if prev_indent :\r\n                indent_val = prev_indent\r\n                # print('当前合并2发的行','上一行：', prev_line[\"text\"],'当前行:',line[\"text\"] )\r\n            else:\r\n                if (px0 >x0):\r\n                    indent_val = px0 - x0\r\n                else:\r\n                    indent_val = 0\r\n            merged[-1][\"indent\"] = indent_val\r\n            end_indent_val = abs(px1 - x1) if (px1 > x1 and abs(px1 - x1) > max_horizontal_gap) else 0\r\n            merged[-1][\"end_indent\"] = end_indent_val\r\n            merged[-1][\"text\"] = prev_line[\"text\"].rstrip() + \" \" + line[\"text\"].lstrip()\r\n\r\n            new_x0 = min(px0, x0)\r\n            new_y0 = min(py0, y0)\r\n            new_x1 = max(px1, x1)\r\n            new_y1 = max(py1, y1)\r\n            merged[-1][\"line_bbox\"] = (new_x0, new_y0, new_x1, new_y1)\r\n            merged[-1][\"total_bold_chars\"] += line[\"total_bold_chars\"]\r\n            merged[-1][\"total_nonbold_chars\"] += line[\"total_nonbold_chars\"]\r\n            merged[-1][\"font_bold\"] = merged[-1][\"total_bold_chars\"] > merged[-1][\"total_nonbold_chars\"]\r\n            merged[-1][\"font_names\"].extend(line[\"font_names\"])\r\n            merged[-1][\"font_names\"] = list(set(merged[-1][\"font_names\"]))\r\n            # print('合并2', indent_val ,end_indent_val,px0,x0,prev_line['indent'],merged[-1][\"text\"])\r\n            # print(\"------\")\r\n\r\n            merged_this_round = True\r\n        elif condition_5:\r\n\r\n            # 如果上一行已经有缩进，直接继承\r\n            if prev_line['indent']:\r\n                indent_val = prev_line['indent']\r\n            else:\r\n                if (px0 > x0) :\r\n                    indent_val = px0 - x0\r\n                else:\r\n                    indent_val = 0\r\n\r\n            # 只在明显缩进时生效，否则为0\r\n\r\n            merged[-1][\"indent\"] = indent_val\r\n\r\n            end_indent_val = abs(px1 - x1) if (px1 > x1 and abs(px1 - x1) > max_horizontal_gap) else 0\r\n            merged[-1][\"end_indent\"] = end_indent_val\r\n            merged[-1][\"text\"] = prev_line[\"text\"].rstrip() + \" \" + line[\"text\"].lstrip()\r\n\r\n            new_x0 = min(px0, x0)\r\n            new_y0 = min(py0, y0)\r\n            new_x1 = max(px1, x1)\r\n            new_y1 = max(py1, y1)\r\n            merged[-1][\"line_bbox\"] = (new_x0, new_y0, new_x1, new_y1)\r\n            merged[-1][\"total_bold_chars\"] += line[\"total_bold_chars\"]\r\n            merged[-1][\"total_nonbold_chars\"] += line[\"total_nonbold_chars\"]\r\n            merged[-1][\"font_bold\"] = merged[-1][\"total_bold_chars\"] > merged[-1][\"total_nonbold_chars\"]\r\n            merged[-1][\"font_names\"].extend(line[\"font_names\"])\r\n            merged[-1][\"font_names\"] = list(set(merged[-1][\"font_names\"]))\r\n            # print('合并5', merged[-1][\"text\"], indent_val ,end_indent_val)\r\n            merged_this_round = True\r\n\r\n        elif condition_3:\r\n            if (x1 - px1) > max_x_distance:\r\n                merged.append(line)\r\n                i += 1\r\n                continue\r\n            width_diff = abs(current_width - prev_width)\r\n            if width_diff <= margin_in_middle / 2.0:\r\n                merged_text = prev_line[\"text\"].rstrip() + \" \" + line[\"text\"].lstrip()\r\n                indent_val = (x0 - px0) if (x0 > px0 and (x0 - px0) > (max_horizontal_gap / 2)) else 0\r\n                merged[-1][\"indent\"] = indent_val\r\n                end_indent_val = abs(px1 - x1) if (px1 > x1 and abs(px1 - x1) > max_horizontal_gap) else 0\r\n                merged[-1][\"end_indent\"] = end_indent_val\r\n                prev_line[\"text\"] = merged_text\r\n\r\n\r\n                new_x0 = min(px0, x0)\r\n                new_y0 = min(py0, y0)\r\n                new_x1 = max(px1, x1)\r\n                new_y1 = max(py1, y1)\r\n                prev_line[\"line_bbox\"] = (new_x0, new_y0, new_x1, new_y1)\r\n                prev_line[\"total_bold_chars\"] += line[\"total_bold_chars\"]\r\n                prev_line[\"total_nonbold_chars\"] += line[\"total_nonbold_chars\"]\r\n                prev_line[\"font_bold\"] = prev_line[\"total_bold_chars\"] > prev_line[\"total_nonbold_chars\"]\r\n                prev_line[\"font_names\"].extend(line[\"font_names\"])\r\n                prev_line[\"font_names\"] = list(set(prev_line[\"font_names\"]))\r\n                # print('合并3', merged[-1][\"text\"])\r\n                merged_this_round = True\r\n            else:\r\n                if (prev_width < current_width) and (px0 > x0):\r\n                    indent_val = (x0 - px0) if (x0 > px0 and (x0 - px0) > (max_horizontal_gap / 2)) else 0\r\n                    merged[-1][\"indent\"] = indent_val\r\n                    end_indent_val = abs(px1 - x1) if (px1 > x1 and abs(px1 - x1) > max_horizontal_gap) else 0\r\n                    merged[-1][\"end_indent\"] = end_indent_val\r\n                    merged_text = prev_line[\"text\"].rstrip() + \" \" + line[\"text\"].lstrip()\r\n                    prev_line[\"text\"] = merged_text\r\n\r\n\r\n                    new_x0 = min(px0, x0)\r\n                    new_y0 = min(py0, y0)\r\n                    new_x1 = max(px1, x1)\r\n                    new_y1 = max(py1, y1)\r\n                    prev_line[\"line_bbox\"] = (new_x0, new_y0, new_x1, new_y1)\r\n                    prev_line[\"total_bold_chars\"] += line[\"total_bold_chars\"]\r\n                    prev_line[\"total_nonbold_chars\"] += line[\"total_nonbold_chars\"]\r\n                    prev_line[\"font_bold\"] = prev_line[\"total_bold_chars\"] > prev_line[\"total_nonbold_chars\"]\r\n                    prev_line[\"font_names\"].extend(line[\"font_names\"])\r\n                    prev_line[\"font_names\"] = list(set(prev_line[\"font_names\"]))\r\n                    # print('合并4', merged[-1][\"text\"])\r\n                    merged_this_round = True\r\n                elif (current_width < prev_width) and (x0 >= px0 + 2):\r\n                    merged.append(line)\r\n                    i += 1\r\n                    continue\r\n                else:\r\n                    if prev_width < current_width:\r\n                        indent_val = (x0 - px0) if (x0 > px0 and (x0 - px0) > (max_horizontal_gap / 2)) else 0\r\n                        merged[-1][\"indent\"] = indent_val\r\n                        end_indent_val = abs(px1 - x1) if (px1 > x1 and abs(px1 - x1) > max_horizontal_gap) else 0\r\n                        merged[-1][\"end_indent\"] = end_indent_val\r\n                        merged_text = prev_line[\"text\"].rstrip() + \" \" + line[\"text\"].lstrip()\r\n                        prev_line[\"text\"] = merged_text\r\n\r\n\r\n                        new_x0 = min(px0, x0)\r\n                        new_y0 = min(py0, y0)\r\n                        new_x1 = max(px1, x1)\r\n                        new_y1 = max(py1, y1)\r\n                        prev_line[\"line_bbox\"] = (new_x0, new_y0, new_x1, new_y1)\r\n                        prev_line[\"total_bold_chars\"] += line[\"total_bold_chars\"]\r\n                        prev_line[\"total_nonbold_chars\"] += line[\"total_nonbold_chars\"]\r\n                        prev_line[\"font_bold\"] = prev_line[\"total_bold_chars\"] > prev_line[\"total_nonbold_chars\"]\r\n                        prev_line[\"font_names\"].extend(line[\"font_names\"])\r\n                        prev_line[\"font_names\"] = list(set(prev_line[\"font_names\"]))\r\n                        merged_this_round = True\r\n\r\n\r\n        elif condition_4:\r\n            merged[-1][\"text\"] = prev_line[\"text\"].rstrip() + \" \" + line[\"text\"].lstrip()\r\n\r\n            new_x0 = min(px0, x0)\r\n            new_y0 = min(py0, y0)\r\n            new_x1 = max(px1, x1)\r\n            new_y1 = max(py1, y1)\r\n            indent_val = (x0 - px0) if (x0 > px0 and (x0 - px0) > (max_horizontal_gap / 2) )else 0\r\n            merged[-1][\"indent\"] = indent_val\r\n            end_indent_val = abs(px1 - x1) if (px1 > x1 and abs(px1 - x1) > max_horizontal_gap) else 0\r\n            merged[-1][\"end_indent\"] = end_indent_val\r\n            merged[-1][\"line_bbox\"] = (new_x0, new_y0, new_x1, new_y1)\r\n            merged[-1][\"total_bold_chars\"] += line[\"total_bold_chars\"]\r\n            merged[-1][\"total_nonbold_chars\"] += line[\"total_nonbold_chars\"]\r\n            merged[-1][\"font_bold\"] = merged[-1][\"total_bold_chars\"] > merged[-1][\"total_nonbold_chars\"]\r\n            merged[-1][\"font_names\"].extend(line[\"font_names\"])\r\n            merged[-1][\"font_names\"] = list(set(merged[-1][\"font_names\"]))\r\n            # print('合并后的indent', merged[-1][\"indent\"])\r\n            merged_this_round = True\r\n\r\n\r\n\r\n        if merged_this_round:\r\n            # 合并成功，i不递增，下一轮继续用当前prev_line和下一行比较\r\n            i += 1\r\n            continue\r\n        else:\r\n            # 没合并，推进下一个\r\n            merged.append(line)\r\n            i += 1\r\n\r\n    return merged\r\n\r\n\r\ndef is_math(font_info_list, text_len, text, font_size):\r\n    \"\"\"\r\n    判断文本是否为数学公式(或长度太短)。\r\n    如果是数学公式，则返回 True。\r\n    如果长度很短且几乎全为数字/标点/符号，则返回 \"abandon\"。\r\n    否则返回 False。\r\n    \"\"\"\r\n\r\n    # 用于去除空格计算长度\r\n    text_length_nospaces = len(text.replace(\" \", \"\"))\r\n\r\n    # 若行整体文本长度很短，且字体集合与“数学字体”有交集，则直接视为 math.暂时改为1.0\r\n    if text_length_nospaces < font_size * 1.0:\r\n        font_set = set(font_info_list)\r\n        if font_set & MATH_FONTS_SET:\r\n            # print('math,', text)\r\n            return True\r\n\r\n    # 如果文字过短，就检查每个字符是否都是数字/标点/符号/空白\r\n    if text_len < 1.5 * font_size:\r\n        all_special_chars = True\r\n        stripped_text = text.strip()\r\n        for ch in stripped_text:\r\n            cat = unicodedata.category(ch)\r\n            # 允许通过的情况：\r\n            # 1. 数字 (cat == 'Nd')\r\n            # 2. 标点 (cat.startswith('P'))\r\n            # 3. 符号 (cat.startswith('S'))，包括 Sm (数学符号)、So (其他符号)\r\n            # 4. 空格或其他空白符 (cat.startswith('Z'))\r\n            #    - 或者你可以直接用 ch.isspace() 来判断空格\r\n            if not (\r\n                    cat == 'Nd'\r\n                    or cat.startswith('P')\r\n                    or cat.startswith('S')\r\n                    or cat.startswith('Z')\r\n            ):\r\n                # 如果发现不属于上述几类，则判定为非“纯数字/标点/符号”\r\n                all_special_chars = False\r\n                break\r\n\r\n        # 如果所有字符都在我们的“允许”范围内，则标记为 \"abandon\"\r\n        if all_special_chars:\r\n            # print(stripped_text, '纯数字/标点/符号，标记为abandon')\r\n            return \"abandon\"\r\n\r\n        return False\r\n\r\n    # 其它情况默认不视为 math\r\n    return False\r\n\r\n\r\ndef merge_adjacent_math_lines(lines):\r\n    \"\"\"\r\n    对相邻行进行多次合并(只需一次遍历即可完成)：\r\n        1) 若两行都为 math，且相邻行 x / y 距离足够小，则合并。\r\n        2) 若两行中有且仅有一行是 math，另一行长度非常短(小于一定阈值)，且 x / y 距离足够小，则先将那行也标记为 math，再合并。\r\n\r\n    其中 x_distance / y_distance 的计算方式为：\r\n        x_distance = min(|px0 - cx1|, |cx0 - px1|)\r\n        y_distance = min(|py0 - cy1|, |cy0 - py1|)\r\n\r\n    注：如果 lines 已按阅读顺序排好(从上至下、从左至右)，则在一次扫描中可以将\r\n        所有可以相邻合并的 math 行都合并完成，而不会漏掉“第一次合并后才新形成\r\n        邻接关系”的情况。算法整体复杂度约为 O(N)。\r\n    \"\"\"\r\n\r\n    if not lines:\r\n        return []\r\n\r\n    def get_font_size(line):\r\n        # 若行的 font_size 为 None，则简单兜底为 10\r\n        return line[\"font_size\"] if line[\"font_size\"] else 10\r\n\r\n    def can_merge(prev_line, curr_line):\r\n        \"\"\"\r\n        判断是否能将 curr_line 合并到 prev_line。\r\n        如果返回 (True, 'BOTH_MATH'/'ONE_MATH_PREV'/'ONE_MATH_CURR'),\r\n        表示可合并并说明是哪种类型；若返回 (False, None) 则不能合并。\r\n        \"\"\"\r\n        px0, py0, px1, py1 = prev_line[\"line_bbox\"]\r\n        cx0, cy0, cx1, cy1 = curr_line[\"line_bbox\"]\r\n\r\n        # x / y 方向上的最近距离\r\n        x_distance = min(abs(px0 - cx1), abs(cx0 - px1))\r\n        y_distance = min(abs(py0 - cy1), abs(cy0 - py1))\r\n\r\n        # 也可以考虑检测部分重叠\r\n        x_distance_overlap = min(abs(px0 - cx0), abs(cx1 - px1))\r\n\r\n        prev_is_math = (prev_line[\"type\"] == \"math\")\r\n        curr_is_math = (curr_line[\"type\"] == \"math\")\r\n        prev_len = prev_line[\"total_bold_chars\"] + prev_line[\"total_nonbold_chars\"]\r\n        curr_len = curr_line[\"total_bold_chars\"] + curr_line[\"total_nonbold_chars\"]\r\n\r\n        fs_p = get_font_size(prev_line)\r\n        fs_c = get_font_size(curr_line)\r\n        max_horizontal_gap = (fs_p + fs_c) / 2.0  # 动态阈值\r\n\r\n        # 条件 1：两行都为 math，并且距离较小\r\n        cond_math_both = (\r\n                prev_is_math\r\n                and curr_is_math\r\n                and (\r\n                        (x_distance < 5 * max_horizontal_gap and y_distance < 3 * max_horizontal_gap)\r\n                        or (x_distance_overlap < 5 * max_horizontal_gap and y_distance < 3 * max_horizontal_gap)\r\n                )\r\n        )\r\n\r\n        # 条件 2：只有一行是 math，且另一行非常短; 距离也小\r\n        cond_one_math_prev = (\r\n                prev_is_math\r\n                and not curr_is_math\r\n                and (curr_len < max_horizontal_gap)\r\n                and (x_distance < 2 * max_horizontal_gap)\r\n                and (y_distance < 1.5 * max_horizontal_gap)\r\n        )\r\n        cond_one_math_curr = (\r\n                not prev_is_math\r\n                and curr_is_math\r\n                and (prev_len < max_horizontal_gap)\r\n                and (x_distance < 2 * max_horizontal_gap)\r\n                and (y_distance < 1.5 * max_horizontal_gap)\r\n        )\r\n\r\n        if cond_math_both:\r\n\r\n            return (True, \"BOTH_MATH\")\r\n        elif cond_one_math_prev:\r\n            return (True, \"ONE_MATH_PREV\")\r\n        elif cond_one_math_curr:\r\n            return (True, \"ONE_MATH_CURR\")\r\n        else:\r\n            return (False, None)\r\n\r\n    def do_merge(prev_line, curr_line, merge_type):\r\n        \"\"\"\r\n        将 curr_line 合并到 prev_line，并根据 merge_type 做必要的类型标记。\r\n        返回合并后的行(即 prev_line)。注意这是原地修改 prev_line。\r\n        \"\"\"\r\n        # 若是 \"ONE_MATH_CURR\"，说明 curr_line 是 math，prev_line 要标记成 math\r\n        if merge_type == \"ONE_MATH_CURR\":\r\n            prev_line[\"type\"] = \"math\"\r\n        # 若是 \"ONE_MATH_PREV\"，说明 prev_line 是 math，要把 curr_line 标记为 math\r\n        elif merge_type == \"ONE_MATH_PREV\":\r\n            curr_line[\"type\"] = \"math\"\r\n        # \"BOTH_MATH\" 不用特别改标记\r\n\r\n        # 合并文本与 bbox\r\n        prev_line[\"text\"] = prev_line[\"text\"].rstrip() + \" \" + curr_line[\"text\"].lstrip()\r\n        px0, py0, px1, py1 = prev_line[\"line_bbox\"]\r\n        cx0, cy0, cx1, cy1 = curr_line[\"line_bbox\"]\r\n        prev_line[\"line_bbox\"] = (\r\n            min(px0, cx0),\r\n            min(py0, cy0),\r\n            max(px1, cx1),\r\n            max(py1, cy1),\r\n        )\r\n\r\n        # 合并字体、字数\r\n        prev_line[\"font_names\"] = list(set(prev_line[\"font_names\"] + curr_line[\"font_names\"]))\r\n        prev_line[\"total_bold_chars\"] += curr_line[\"total_bold_chars\"]\r\n        prev_line[\"total_nonbold_chars\"] += curr_line[\"total_nonbold_chars\"]\r\n\r\n        return prev_line\r\n\r\n    new_lines = []\r\n    for curr_line in lines:\r\n        # 尝试和栈顶行合并, 若可以则合并再继续看是否能与更早的行合并(链式)\r\n        while new_lines:\r\n            can_merge_flag, merge_type = can_merge(new_lines[-1], curr_line)\r\n            if can_merge_flag:\r\n                # 弹出栈顶行, 和 curr_line 合并\r\n                merged = do_merge(new_lines.pop(), curr_line, merge_type)\r\n                # 合并后要让 merged 作为“新的 curr_line”再尝试和更早的行合并\r\n                curr_line = merged\r\n            else:\r\n                # 无法和栈顶合并, 则停止\r\n                break\r\n\r\n        # 最后把 curr_line (可能是合并后的结果) 压入栈\r\n        new_lines.append(curr_line)\r\n\r\n    return new_lines\r\n\r\n\r\ndef get_new_blocks(page, pdf_path=None, page_num=None):\r\n    \"\"\"\r\n    从指定 PDF 的某页提取文本行(blocks->lines->spans)，\r\n    做基础的过滤和 bbox 合并后，得到行数据 lines_data。\r\n    然后：\r\n      1) horizontal_merge(): 水平合并\r\n      2) merge_lines(): 垂直合并\r\n      3) 基于“行号 + 块号 + 行类型 + 字符长度”等信息构建临时数据结构，判断整块是否可疑\r\n      4) 若可疑（小于某字符数且含 math 行）则整块标记为 math\r\n      5) 最后合并相邻 math 行\r\n      6) 打印(或返回)结果\r\n    \"\"\"\r\n\r\n    try:\r\n\r\n        if pdf_path and page_num:\r\n            pdf_document = fitz.open(pdf_path)\r\n            if page_number < 1 or page_number > pdf_document.page_count:\r\n                print(f\"页码 {page_number} 超出范围（1 - {pdf_document.page_count}）\")\r\n                return\r\n\r\n            page = pdf_document[page_number - 1]\r\n\r\n        blocks = page.get_text(\"dict\")[\"blocks\"]\r\n        lines_data = []\r\n\r\n        # ============= 读取并初步整理行信息 =============\r\n        for i, block in enumerate(blocks, start=1):\r\n            if 'lines' not in block:\r\n                continue\r\n            for line in block[\"lines\"]:\r\n                spans = line.get(\"spans\", [])\r\n                if not spans:\r\n                    continue\r\n\r\n                filtered_spans = [span for span in spans if span.get(\"text\", \"\").strip() != \"\"]\r\n                if not filtered_spans:\r\n                    continue\r\n\r\n                # 1) 计算这一行的 bbox（合并 filtered_spans 的 bbox）\r\n                x0_list, y0_list, x1_list, y1_list = [], [], [], []\r\n                for span in filtered_spans:\r\n                    if \"bbox\" in span:\r\n                        sbbox = span[\"bbox\"]\r\n                        x0_list.append(sbbox[0])\r\n                        y0_list.append(sbbox[1])\r\n                        x1_list.append(sbbox[2])\r\n                        y1_list.append(sbbox[3])\r\n\r\n                if x0_list and y0_list and x1_list and y1_list:\r\n                    new_line_bbox = (\r\n                        min(x0_list), min(y0_list),\r\n                        max(x1_list), max(y1_list),\r\n                    )\r\n                else:\r\n                    new_line_bbox = line[\"bbox\"]\r\n\r\n                # 2) 拼接文本，并收集字体等信息\r\n                full_text = \"\"\r\n                font_sizes = set()\r\n                colors = set()\r\n                bold_flags = []\r\n                longest_span_length = 0\r\n                longest_span_font = None\r\n                font_names_set = set()\r\n\r\n                for span in filtered_spans:\r\n                    span_text = span[\"text\"]\r\n                    full_text += span_text\r\n                    font_sizes.add(span[\"size\"])\r\n                    colors.add(span[\"color\"])\r\n\r\n                    this_font_name = span.get(\"font\", \"\")\r\n                    font_names_set.add(this_font_name)\r\n\r\n                    # 判断是否加粗\r\n                    is_bold = span.get(\"face\", {}).get(\"bold\", False)\r\n                    if not is_bold:\r\n                        # 额外判断关键字\r\n                        bold_keywords = [\"bold\", \"cmbx\", \"heavy\", \"demi\"]\r\n                        lower_font_name = this_font_name.lower()\r\n                        for kw in bold_keywords:\r\n                            if kw in lower_font_name:\r\n                                is_bold = True\r\n                                break\r\n                    bold_flags.append(is_bold)\r\n\r\n                    # 统计 span 的有效字符长度(去除空格)\r\n                    stripped_span_text = span_text.strip()\r\n                    span_len = len(stripped_span_text)\r\n                    if span_len > longest_span_length:\r\n                        longest_span_length = span_len\r\n                        longest_span_font = this_font_name\r\n\r\n                line_is_bold = any(bold_flags)\r\n                stripped_text = full_text.strip()\r\n                if not stripped_text:\r\n                    continue\r\n\r\n                # 3) 若行首有“•”，则去掉并向右偏移\r\n                if stripped_text.startswith(\"•\"):\r\n                    stripped_text = stripped_text[1:].lstrip()\r\n                    new_line_bbox = (\r\n                        new_line_bbox[0] + 10,\r\n                        new_line_bbox[1],\r\n                        new_line_bbox[2],\r\n                        new_line_bbox[3],\r\n                    )\r\n                    full_text = stripped_text\r\n\r\n                # 4) 计算行的旋转角度(吸附至0/90/180/270/360)\r\n                raw_angle = math.degrees(\r\n                    math.atan2(\r\n                        line.get(\"dir\", [1.0, 0.0])[1],\r\n                        line.get(\"dir\", [1.0, 0.0])[0]\r\n                    )\r\n                )\r\n                angle = snap_angle_func(raw_angle)\r\n\r\n                # 5) 使用最长 span 的字体名称作为行的 font_name（代表字体）\r\n                chosen_font_name = longest_span_font if longest_span_font else None\r\n                line_len = len(full_text)\r\n\r\n                # 初始化该行的粗体/非粗体字符累加\r\n                if line_is_bold:\r\n                    tb = line_len  # total_bold_chars\r\n                    tnb = 0\r\n                else:\r\n                    tb = 0\r\n                    tnb = line_len\r\n\r\n                line_data = {\r\n                    \"block_index\": i,\r\n                    \"line_bbox\": new_line_bbox,\r\n                    \"text\": full_text,\r\n                    \"font_size\": (list(font_sizes)[0] if font_sizes else None),\r\n                    \"font_color\": (list(colors)[0] if colors else None),\r\n                    \"font_name\": chosen_font_name,  # 仅代表字体名称\r\n                    \"font_names\": list(font_names_set),  # 全部子span字体集合\r\n                    \"rotation_angle\": angle,\r\n                    \"type\": \"plain_text\",  # 初始行类型\r\n                    \"font_bold\": line_is_bold,\r\n                    \"indent\": 0,\r\n                    \"end_indent\": 0,\r\n                    \"total_bold_chars\": tb,\r\n                    \"total_nonbold_chars\": tnb\r\n                }\r\n                lines_data.append(line_data)\r\n\r\n        if not lines_data:\r\n            print(\"该页面没有提取到任何文本行\")\r\n            return\r\n\r\n        # ============= (1) 水平合并 =============\r\n        merged_horizontally = horizontal_merge(\r\n            lines_data,\r\n            max_horizontal_gap=20,\r\n            max_y_diff=5,\r\n            check_font_size=False,\r\n            check_font_name=False,\r\n            check_font_color=False\r\n        )\r\n\r\n        # ============= (2) 垂直合并 =============\r\n        merged_final = merge_lines(\r\n            merged_horizontally,\r\n            check_font_size=False,\r\n            check_font_name=False,\r\n            check_font_color=False,\r\n            check_same_block=False\r\n        )\r\n\r\n        # ============= (3) 基于合并后行信息，构建临时数据结构 =============\r\n        #    存储块号 -> [ (行在 merged_final 列表中的索引, 字符长度, 行类型) ... ]，\r\n        #    并统计块内总字符数。\r\n        temp_block_dict = defaultdict(lambda: {\r\n            'lines': [],  # [(merged_final_idx, line_type, text_len), ...]\r\n            'total_chars': 0  # 整个 block 的字符串长度总和\r\n        })\r\n\r\n        for idx, line_info in enumerate(merged_final):\r\n            # 计算该行字符长度\r\n            text_len = line_info['total_bold_chars'] + line_info['total_nonbold_chars']\r\n            # 判断是否是 math（行类型先设置，这里只是初步）\r\n            final_text = line_info['text'] or ''\r\n\r\n            if final_text:\r\n                result = is_math(line_info['font_names'], text_len, final_text, line_info['font_size'])\r\n                if result == True:\r\n                    line_info['type'] = 'math'\r\n                elif result == \"abandon\":\r\n                    line_info['type'] = 'abandon'\r\n            else:\r\n                line_info['type'] = 'abandon'\r\n\r\n            block_idx = line_info['block_index']\r\n            temp_block_dict[block_idx]['lines'].append(\r\n                (idx, line_info['type'], text_len)\r\n            )\r\n            temp_block_dict[block_idx]['total_chars'] += text_len\r\n\r\n        # ============= (4) 根据块信息，若块内总字符数 < 50 且含有一行 math，则全块标记为 math =============\r\n        for b_idx, block_data in temp_block_dict.items():\r\n            total_chars = block_data['total_chars']\r\n            lines_in_block = block_data['lines']\r\n            # 判断该 block 是否含 math\r\n            has_math_in_block = any(ln_type == 'math' for (_, ln_type, _) in lines_in_block)\r\n            if (total_chars < 30) and has_math_in_block:\r\n                # 将该 block 下所有行都标记为 math\r\n                for (merged_idx, _, _) in lines_in_block:\r\n                    merged_final[merged_idx]['type'] = 'math'\r\n\r\n        # ============= (5) 对相邻 math 行进行二次合并 =============\r\n        merged_final = merge_adjacent_math_lines(merged_final)\r\n\r\n        # ============= (6) 打印结果并构造返回值 ============= 115行\r\n        new_blocks = []\r\n        for idx, line_info in enumerate(merged_final, start=1):\r\n            # print(f\"行 {idx}:\")\r\n            # print(f\" 所属块序号(block_index): {line_info['block_index']}\")\r\n            # print(f\" 行坐标(line_bbox): {line_info['line_bbox']}\")\r\n            # print(f\" 文本(text): {line_info['text']}\")\r\n            # print(f\" 字体大小(font_size): {line_info['font_size']}\")\r\n            # print(f\" 字体颜色(font_color): {line_info['font_color']}\")\r\n            # print(f\" 代表字体名称(font_name): {line_info['font_name']}\")\r\n            # print(f\" 所有字体名称(all_font_names): {line_info['font_names']}\")\r\n            # print(f\" 旋转角度(rotation_angle): {line_info['rotation_angle']}°\")\r\n            # print(f\" 字体加粗(font_bold): {line_info['font_bold']}\")\r\n            # print(f\" 缩进(indent): {line_info['indent']}\")\r\n            # print(f\" 末尾缩进(indent): {line_info['end_indent']}\")\r\n            # print(f\" total_bold_chars = {line_info['total_bold_chars']}, total_nonbold_chars = {line_info['total_nonbold_chars']}\")\r\n            # print(f\" 行类型(type): {line_info['type']}\")\r\n            # print(\"-\" * 50)\r\n\r\n            # 排除被标记为 abandon 的块\r\n            if line_info['type'] != 'abandon':\r\n                new_blocks.append([\r\n                    line_info['text'],\r\n                    tuple(line_info['line_bbox']),\r\n                    line_info['type'],\r\n                    line_info['rotation_angle'],\r\n                    line_info['font_color'],\r\n                    line_info['indent'],\r\n                    line_info['font_bold'],\r\n                    line_info['font_size'],\r\n                    line_info['end_indent']\r\n                ])\r\n\r\n        return new_blocks\r\n\r\n    except Exception as e:\r\n        print(f\"发生错误: {e}\")\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    b = datetime.datetime.now()\r\n    pdf_path = \"m2.pdf\"  # 换成你的 PDF 文件路径\r\n    page_number = 4 # 换成想处理的页码\r\n    z = get_new_blocks(page=None, pdf_path=pdf_path, page_num=page_number)\r\n    print(\"最终返回的 new_blocks:\", z)\r\n    e = datetime.datetime.now()\r\n    elapsed_time = (e - b).total_seconds()\r\n    print(f\"运行时间: {elapsed_time} 秒\")\r\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"UTF-8\">\n    <title>PolyglotPDF</title>\n    <link rel=\"stylesheet\" href=\"./static/main.css\">\n    <link rel=\"stylesheet\" href=\"./static/setup.css\">\n    <link rel=\"icon\" href=\"./static/PolyglotPDF.png?v=1\" type=\"image/png\">\n\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css\">\n</head>\n\n<body>\n    <div class=\"container\">\n        <div class=\"sidebar\">\n            <h2 data-lang-key=\"1\">Library</h2>\n            <ul class=\"sidebar-menu\">\n                <li><a data-lang-key=\"2\" href=\"#\" class=\"active\" onclick=\"showHome()\">Homepage</a></li>\n                <li><a data-lang-key=\"3\" href=\"#\" onclick=\"showAllRecent()\">Recent Reading</a></li>\n                <li><a data-lang-key=\"4\" href=\"#\" onclick=\"showSetup()\">Setup steps</a></li>\n                <li><a data-lang-key=\"5\" href=\"#\">Search</a></li>\n                <li><a data-lang-key=\"6\" href=\"#\">Folders</a></li>\n            </ul>\n        </div>\n\n        <div class=\"main-content\">\n            <div class=\"header\">\n                <h1 data-lang-key=\"7\" id=\"recentread\">Recent Reading</h1>\n                <div class=\"header-buttons\">\n                    <!-- HTML -->\n                    <div class=\"button-group\">\n                        <div class=\"language-switch\">\n                            <select id=\"languageSelect\" class=\"nice-language-select\">\n                                <option value=\"en\">English</option>\n                                <option value=\"zh\">中文</option>\n\n                            </select>\n                        </div>\n                        <button class=\"action-button \" onclick=\"showBatchModal()\">\n                            <i class=\"fa-solid fa-list-check\"></i>\n                            <span data-lang-key=\"8\">Batch</span>\n                        </button>\n\n                    </div>\n\n                    <button data-lang-key=\"10\" class=\"add-button\" onclick=\"showUpload()\">+ Add Article</button>\n\n                </div>\n            </div>\n\n            <div id=\"viewAllSection\" class=\"recent-header\">\n                <h2 data-lang-key=\"12\" id='count_article'>Articles in Total:</h2>\n                <a data-lang-key=\"13\" href=\"#\" class=\"view-all\" onclick=\"showAllRecent()\">View All ></a>\n            </div>\n\n            <div class=\"article-grid\" id=\"articleContainer\">\n            </div>\n\n            <div class=\"t-container\" id=\"t-container\">\n                <div class=\"t-header-container\">\n                    <h1 data-lang-key=\"14\">配置文件编辑器</h1>\n                    <button data-lang-key=\"15\" class=\"t-save-btn\" id=\"saveall\" onclick=\"saveall()\">保存所有修改</button>\n                </div>\n\n                <h3 data-lang-key=\"16\"\n                    style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-weight: normal;margin-left: 5px; margin-bottom: 10px;\">\n                    国内大语言模型API申请：</h3>\n\n                <div\n                    style=\"display: flex; justify-content: space-between; gap: 40px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; padding: 20px;\">\n                    <div style=\"flex: 1; padding: 20px; background-color: #f8f9fa; border-radius: 8px;\">\n                        <p data-lang-key=\"17\" style=\"margin: 0 0 15px 0;\">通过火山引擎平台申请:</p>\n                        <ul style=\"list-style-type: none; padding: 0; margin: 0;\">\n                            <li data-lang-key=\"18\" style=\"margin-bottom: 10px;\">申请地址: <a\n                                    href=\"https://www.volcengine.com/product/doubao/\"\n                                    style=\"color: #0066cc; text-decoration: none;\">火山引擎-豆包</a></li>\n                            <li data-lang-key=\"19\">支持模型: 豆包(Doubao)、Deepseek系列模型</li>\n                        </ul>\n                    </div>\n\n                    <div style=\"flex: 1; padding: 20px; background-color: #f8f9fa; border-radius: 8px;\">\n                        <p data-lang-key=\"20\" style=\"margin: 0 0 15px 0;\">通过阿里云平台申请:</p>\n                        <ul style=\"list-style-type: none; padding: 0; margin: 0;\">\n                            <li data-lang-key=\"21\" style=\"margin-bottom: 10px;\">申请地址: <a\n                                    href=\"https://cn.aliyun.com/product/tongyi?from_alibabacloud=&utm_content=se_1019997984\"\n                                    style=\"color: #0066cc; text-decoration: none;\">阿里云-通义千问</a></li>\n                            <li data-lang-key=\"22\">支持模型: Qwen-Max、Qwen-Plus等系列模型</li>\n                        </ul>\n                    </div>\n                </div>\n\n\n                <div class=\"t-section\">\n                    <div class=\"t-input-group\">\n                        <label data-lang-key=\"23\">count:</label>\n                        <span class=\"t-count-display\" id=\"t-count\"></span>\n                    </div>\n                </div>\n                <div class=\"t-section\">\n                    <div class=\"t-input-group\">\n                        <label data-lang-key=\"24\">PPC:</label>\n                        <input type=\"text\" class=\"t-ppc\" id=\"t-ppc\" />\n                    </div>\n                </div>\n                <div class=\"t-section\">\n                    <div class=\"t-section-header\">\n                        <h3 data-lang-key=\"25\">翻译模型API</h3>\n                        <button class=\"t-toggle-btn\">+</button>\n                    </div>\n                    <div class=\"t-content\" id=\"t-translation-services\">\n                        <!-- 翻译服务的内容将通过JavaScript动态生成 -->\n                    </div>\n                </div>\n                <div class=\"t-section\">\n                    <div class=\"t-section-header\">\n                        <h3 data-lang-key=\"26\">OCR 服务</h3>\n                        <button class=\"t-toggle-btn\">+</button>\n                    </div>\n                    <div class=\"t-content\" id=\"t-ocr-services\">\n                        <!-- OCR服务的内容将通过JavaScript动态生成 -->\n                    </div>\n                </div>\n                <div class=\"t-section\" style=\"display: grid; grid-template-columns: 1fr 1fr; gap: 7px;\">\n                    <div class=\"t-left\">\n                        <div class=\"t-section-header\">\n                            <h3 data-lang-key=\"27\">默认配置</h3>\n                            <button class=\"t-toggle-btn\">+</button>\n                        </div>\n                        <div class=\"t-content\" id=\"t-default-services\">\n                            <!-- 默认配置的内容将通过JavaScript动态生成 -->\n                        </div>\n                    </div>\n\n                    <div class=\"t-right\">\n                        <div class=\"info-box\">\n                            <p data-lang-key=\"47\" class=\"info-text\">\n\n                            </p>\n\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n        </div>\n    </div>\n    <div class=\"upload-container\" id=\"uploadModal\">\n        <div class=\"upload-content\" id=\"upload_content-1\">\n            <div class=\"upload-header\">\n                <h2 data-lang-key=\"28\">Add Literature to Your Reading List </h2>\n                <button class=\"close-btn\" style=\"position: absolute!important;top: 0px!important;right: 0px!important;\"\n                    onclick=\"closeUploadModal()\">×</button>\n            </div>\n\n            <div class=\"upload-files-list\" id=\"uploadFilesList\"></div>\n\n            <div class=\"upload-area\" id=\"dropZone\">\n                <input type=\"file\" id=\"fileInput\" multiple accept=\".pdf,.xps,.epub,.fb2,.cbz,.mobi\"\n                    style=\"display: none;\">\n                <i class=\"fa-solid fa-file-arrow-up\"></i>\n                <p class=\"upload-text\" data-lang-key=\"29\">Drag and drop your files here or<span class=\"upload-link\"\n                        onclick=\"triggerFileInput()\"> click to upload</span></p>\n                <p class=\"upload-hint\" data-lang-key=\"30\">Up to 12 files at a time, each with a maximum of 200MB</p>\n                <p class=\"upload-formats\" data-lang-key=\"31\">Supported : PDF, XPS, EPUB, FB2, CBZ, MOBI</p>\n            </div>\n\n\n            <div class=\"language-selection\" id=\"languageSelection\" style=\"display: none;\">\n                <div style=\"display: flex; justify-content: space-between; width: 99%; gap: 20px; margin-bottom: 5px;\">\n                    <div class=\"source-lang\" style=\"flex: 1;\">\n                        <select id=\"sourceLang\"\n                            style=\"width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 14px;\">\n                            <option value=\"auto\" selected>auto</option>\n                            <option value=\"zh\">中文简体</option>\n                            <option value=\"zh-TW\">中文繁體</option>\n                            <option value=\"en\">English</option>\n                            <option value=\"ja\">日本語</option>\n                            <option value=\"ko\">한국어</option>\n                            <option value=\"fr\">Français</option>\n                            <option value=\"de\">Deutsch</option>\n                            <option value=\"es\">Español</option>\n                            <option value=\"it\">Italiano</option>\n                            <option value=\"ru\">Русский</option>\n                            <option value=\"pt\">Português</option>\n                            <option value=\"nl\">Nederlands</option>\n                            <option value=\"pl\">Polski</option>\n                            <option value=\"ar\">العربية</option>\n                            <option value=\"hi\">हिन्दी</option>\n                            <option value=\"tr\">Türkçe</option>\n                            <option value=\"th\">ไทย</option>\n                            <option value=\"vi\">Tiếng Việt</option>\n                            <option value=\"id\">Bahasa Indonesia</option>\n                            <option value=\"ms\">Bahasa Melayu</option>\n                            <option value=\"fa\">فارسی</option>\n                            <option value=\"he\">עברית</option>\n                            <option value=\"el\">Ελληνικά</option>\n                            <option value=\"sv\">Svenska</option>\n                        </select>\n                    </div>\n                    <div class=\"target-lang\" style=\"flex: 1;\">\n                        <select id=\"targetLang\"\n                            style=\"width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 14px;\">\n                            <option value=\"zh\">中文简体</option>\n                            <option value=\"zh-TW\">中文繁體</option>\n                            <option value=\"en\">English</option>\n                            <option value=\"ja\">日本語</option>\n                            <option value=\"ko\">한국어</option>\n                            <option value=\"fr\">Français</option>\n                            <option value=\"de\">Deutsch</option>\n                            <option value=\"es\">Español</option>\n                            <option value=\"it\">Italiano</option>\n                            <option value=\"ru\">Русский</option>\n                            <option value=\"pt\">Português</option>\n                            <option value=\"nl\">Nederlands</option>\n                            <option value=\"pl\">Polski</option>\n                            <option value=\"ar\">العربية</option>\n                            <option value=\"hi\">हिन्दी</option>\n                            <option value=\"tr\">Türkçe</option>\n                            <option value=\"th\">ไทย</option>\n                            <option value=\"vi\">Tiếng Việt</option>\n                            <option value=\"id\">Bahasa Indonesia</option>\n                            <option value=\"ms\">Bahasa Melayu</option>\n                            <option value=\"fa\">فارسی</option>\n                            <option value=\"he\">עברית</option>\n                            <option value=\"el\">Ελληνικά</option>\n                            <option value=\"sv\">Svenska</option>\n                        </select>\n                    </div>\n                    <div class=\"target-lang\" style=\"flex: 1;\">\n                        <button class=\"next-step-btn\" onclick=\"handleNextStep()\"\n                            style=\"width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 2px; font-size: 14px;\">Next\n                        </button>\n                    </div>\n                </div>\n\n            </div>\n            <!-- 成功提示界面 -->\n\n        </div>\n        <div class=\"upload-content\" style=\"display: none!important; height:50% \" id=\"upload_content-2\">\n            <div class=\"upload-header\">\n                <div class=\"success-message\" id=\"successMessage\"\n                    style=\"display: flex; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center;\">\n                    <div class=\"success-icon\" style=\"font-size: 80px; color: #4CAF50; margin-bottom: 20px;\">✓</div>\n                    <p class=\"success-text\" style=\"font-size: 18px; color: #333;\" data-lang-key=\"32\">Please wait\n                        patiently, you can check the translation progress under the recent reading tab.</p>\n                </div>\n                <button class=\"close-btn\" style=\"position: absolute!important;top: 0px!important;right: 0px!important;\"\n                    onclick=\"closeUploadModal()\">×</button>\n            </div>\n\n\n            <!-- 成功提示界面 -->\n\n        </div>\n\n    </div>\n\n\n\n\n\n    </div>\n\n    </div>\n    <div id=\"record_show_staute\" data-value=\"true\" style=\"display: none;\">\n    </div>\n\n    <!-- 批量操作弹窗 -->\n    <div id=\"batchModal\" class=\"upload-container\">\n        <div class=\"batch-content\">\n            <div class=\"batch-header\">\n                <h2 data-lang-key=\"42\">Batch Management</h2>\n                <button class=\"close-btn\" onclick=\"closeBatchModal()\">×</button>\n            </div>\n\n            <div class=\"batch-controls\">\n                <button data-lang-key=\"43\" class=\"action-button\" onclick=\"toggleSelectAll()\">Select All</button>\n                <button data-lang-key=\"44\" class=\"action-button\" onclick=\"handleBatchDelete()\">Delete</button>\n                <button data-lang-key=\"45\" class=\"action-button\" onclick=\"handleMindMap()\">Mind Map</button>\n                <button data-lang-key=\"46\" class=\"action-button\" onclick=\"handleSummary()\">Summary</button>\n            </div>\n\n            <!-- 这里放置盛放卡片的容器，支持滚动  -->\n            <div class=\"batch-grid\" id=\"batchGrid\"></div>\n        </div>\n    </div>\n    <!-- 基础库 -->\n    <script src=\"./static/i18n.js\"></script>\n    <script>\n        // 获取浏览器语言的简易函数\n        function getBrowserLanguage() {\n            const lang = (navigator.language || navigator.userLanguage).toLowerCase();\n            if (lang.includes('zh')) {\n                return 'zh';\n            }\n            return 'en';\n        }\n\n        // 根据传入 lang 对整页进行翻译\n        function applyTranslations(lang) {\n            document.querySelectorAll('[data-lang-key]').forEach(el => {\n                const key = el.getAttribute('data-lang-key');\n                if (i18n[lang] && i18n[lang][key]) {\n                    // 仅更新文本，不动外部标签的 class、样式\n                    el.innerHTML = i18n[lang][key];\n                }\n            });\n        }\n\n        // 当页面加载时，先读 localStorage，如无才用浏览器语言\n        window.addEventListener('load', () => {\n            const storedLang = localStorage.getItem('userLang');\n            let langToUse;\n\n            if (storedLang) {\n                langToUse = storedLang;\n            } else {\n                langToUse = getBrowserLanguage();\n            }\n\n            // 设置下拉框默认选项\n            document.getElementById('languageSelect').value = langToUse;\n\n            // 应用翻译\n            applyTranslations(langToUse);\n        });\n\n        // 下拉框切换事件\n        document.getElementById('languageSelect').addEventListener('change', function () {\n            const selectedLang = this.value;\n            // 翻译\n            applyTranslations(selectedLang);\n            // 存储到 localStorage\n            localStorage.setItem('userLang', selectedLang);\n        });\n    </script>\n    <script src=\"./static/1.js\"></script>\n\n    <!-- 公共文件 -->\n    <script src=\"./static/2.js\"></script>\n\n    <!-- 业务代码 -->\n    <script src=\"./static/3.js\"></script>\n    <script src=\"./static/4.js\"></script>\n    <script src=\"./static/setup.js\"></script>\n\n\n</body>\n\n</html>"
  },
  {
    "path": "languagedetect.py",
    "content": "\n\ntext = \"今日は（こんにちは）\"\n\n# 方法2：直接使用detect\nfrom langdetect import detect\nlang_code = detect(text)\nprint(lang_code)  # 输出: ja\n"
  },
  {
    "path": "load_config.py",
    "content": "import json\r\nimport os\r\nimport sys\r\nimport time\r\nfrom typing import Any, Dict, List, Optional\r\nfrom pathlib import Path\r\n\r\n\r\n\r\nclass ConfigError(Exception):\r\n    \"\"\"配置文件操作相关的自定义异常\"\"\"\r\n    pass\r\n\r\n\r\n# 添加获取应用数据目录的函数\r\ndef get_app_data_dir():\r\n    \"\"\"获取应用数据目录，确保跨平台兼容性\"\"\"\r\n    if getattr(sys, 'frozen', False):\r\n        # 打包后的应用\r\n        if sys.platform == 'darwin':  # macOS\r\n            # 在macOS上使用用户的Application Support目录\r\n            app_data = os.path.join(os.path.expanduser('~/Library/Application Support'), 'EbookTranslation')\r\n        elif sys.platform == 'linux':  # Linux\r\n            # 在Linux上使用~/.local/share目录\r\n            app_data = os.path.join(os.path.expanduser('~/.local/share'), 'EbookTranslation')\r\n        else:  # Windows或其他\r\n            # 在Windows上使用应用程序所在目录\r\n            app_data = os.path.dirname(sys.executable)\r\n    else:\r\n        # 开发环境\r\n        app_data = os.path.dirname(os.path.abspath(__file__))\r\n\r\n    # 确保目录存在\r\n    os.makedirs(app_data, exist_ok=True)\r\n    return app_data\r\n\r\n\r\n# 设置应用数据目录\r\nAPP_DATA_DIR = get_app_data_dir()\r\nprint('数据目录',APP_DATA_DIR)\r\n\r\n\r\ndef get_file_path(filename: str) -> Path:\r\n    \"\"\"\r\n    获取配置文件的完整路径，优先使用APP_DATA_DIR中的文件\r\n\r\n    Args:\r\n        filename: 配置文件名\r\n\r\n    Returns:\r\n        Path: 配置文件的完整路径\r\n    \"\"\"\r\n    # 首先检查APP_DATA_DIR中是否有该文件\r\n    app_data_file = os.path.join(APP_DATA_DIR, filename)\r\n    if os.path.exists(app_data_file):\r\n        return Path(app_data_file)\r\n\r\n    # 如果APP_DATA_DIR中没有，则使用当前目录的文件\r\n    return Path(__file__).parent / filename\r\n\r\n\r\ndef read_json_file(filename: str) -> Any:\r\n    \"\"\"\r\n    读取JSON文件，优先使用APP_DATA_DIR中的文件\r\n\r\n    Args:\r\n        filename: 要读取的文件名\r\n\r\n    Returns:\r\n        解析后的JSON数据\r\n\r\n    Raises:\r\n        ConfigError: 当文件读取或解析失败时\r\n    \"\"\"\r\n    try:\r\n        file_path = get_file_path(filename)\r\n        with open(file_path, 'r', encoding='utf-8') as f:\r\n            return json.load(f)\r\n    except Exception as e:\r\n        raise ConfigError(f\"Error reading {filename}: {str(e)}\")\r\n\r\n\r\ndef write_json_file(filename: str, data: Any) -> None:\r\n    \"\"\"\r\n    写入JSON文件到APP_DATA_DIR\r\n\r\n    Args:\r\n        filename: 要写入的文件名\r\n        data: 要写入的数据\r\n\r\n    Raises:\r\n        ConfigError: 当文件写入失败时\r\n    \"\"\"\r\n    try:\r\n        # 始终写入到APP_DATA_DIR\r\n        file_path = os.path.join(APP_DATA_DIR, filename)\r\n        with open(file_path, 'w', encoding='utf-8') as f:\r\n            json.dump(data, f, ensure_ascii=False, indent=2)\r\n    except Exception as e:\r\n        raise ConfigError(f\"Error writing {filename}: {str(e)}\")\r\n\r\n\r\n# 添加配置文件的最后修改时间和缓存\r\n_config_last_modified_time = 0\r\n_config_cache = None\r\n\r\ndef load_config(force_reload=False) -> Optional[Dict]:\r\n    \"\"\"\r\n    加载主配置文件，优先使用APP_DATA_DIR中的文件\r\n    每次调用都会检查文件是否被修改，如果被修改则重新加载\r\n\r\n    Args:\r\n        force_reload: 是否强制重新加载，忽略缓存\r\n\r\n    Returns:\r\n        Dict: 配置数据，如果加载失败则返回None\r\n    \"\"\"\r\n    global _config_last_modified_time, _config_cache\r\n    \r\n    try:\r\n        # 确定配置文件路径\r\n        app_config_path = os.path.join(APP_DATA_DIR, \"config.json\")\r\n        \r\n        # 检查文件是否存在于APP_DATA_DIR\r\n        if not os.path.exists(app_config_path):\r\n            # 如果不存在，从当前目录复制\r\n            config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), \"config.json\")\r\n            if os.path.exists(config_path):\r\n                with open(config_path, \"r\", encoding=\"utf-8\") as f:\r\n                    data = json.load(f)\r\n                # 将config.json复制到APP_DATA_DIR\r\n                with open(app_config_path, \"w\", encoding=\"utf-8\") as f:\r\n                    json.dump(data, f, ensure_ascii=False, indent=2)\r\n            else:\r\n                print(\"Error: config.json not found in current directory\")\r\n                return None\r\n\r\n        # 获取文件的最后修改时间\r\n        current_mtime = os.path.getmtime(app_config_path)\r\n        \r\n        # 如果文件被修改或强制重新加载，则重新读取\r\n        if force_reload or current_mtime > _config_last_modified_time or _config_cache is None:\r\n            print(f\"重新加载配置文件，上次修改时间: {time.ctime(current_mtime)}\")\r\n            with open(app_config_path, \"r\", encoding=\"utf-8\") as f:\r\n                _config_cache = json.load(f)\r\n            _config_last_modified_time = current_mtime\r\n        \r\n        return _config_cache\r\n    \r\n    except Exception as e:\r\n        print(f\"Error loading config: {str(e)}\")\r\n        return None\r\n\r\n\r\ndef load_recent() -> Optional[List]:\r\n    \"\"\"\r\n    加载最近记录文件，优先使用APP_DATA_DIR中的文件\r\n\r\n    Returns:\r\n        List: 最近记录数据，如果加载失败则返回None\r\n    \"\"\"\r\n    try:\r\n        # 检查APP_DATA_DIR中是否有recent.json\r\n        app_recent_path = os.path.join(APP_DATA_DIR, \"recent.json\")\r\n        if os.path.exists(app_recent_path):\r\n            with open(app_recent_path, \"r\", encoding=\"utf-8\") as f:\r\n                return json.load(f)\r\n        else:\r\n            # 如果APP_DATA_DIR中没有，则使用当前目录的recent.json\r\n            recent_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), \"recent.json\")\r\n            with open(recent_path, \"r\", encoding=\"utf-8\") as f:\r\n                data = json.load(f)\r\n            # 首次访问时，将recent.json复制到APP_DATA_DIR\r\n            with open(app_recent_path, \"w\", encoding=\"utf-8\") as f:\r\n                json.dump(data, f, ensure_ascii=False, indent=2)\r\n            return data\r\n    except Exception as e:\r\n        print(f\"Error loading recent data: {str(e)}\")\r\n        return None\r\n\r\n\r\ndef add_new_entry(new_entry: Dict) -> bool:\r\n    \"\"\"\r\n    添加新记录\r\n\r\n    Args:\r\n        new_entry: 要添加的新记录\r\n\r\n    Returns:\r\n        bool: 操作是否成功\r\n    \"\"\"\r\n    try:\r\n        config = load_recent()\r\n        if config is None:\r\n            return False\r\n\r\n        config.append(new_entry)\r\n        write_json_file('recent.json', config)\r\n        return True\r\n    except ConfigError as e:\r\n        print(f\"Error adding new entry: {str(e)}\")\r\n        return False\r\n\r\n\r\ndef update_count() -> bool:\r\n    \"\"\"\r\n    更新计数器\r\n\r\n    Returns:\r\n        bool: 操作是否成功\r\n    \"\"\"\r\n    try:\r\n        config = load_config()\r\n        print('从cofig.json加载', config['count'])\r\n        if config is None:\r\n            return False\r\n\r\n        config[\"count\"] += 1\r\n        write_json_file('config.json', config)\r\n        return True\r\n    except ConfigError as e:\r\n        print(f\"Error updating count: {str(e)}\")\r\n        return False\r\n\r\n\r\ndef update_file_status(index: int, read: Optional[bool] = None, statue: Optional[str] = None) -> bool:\r\n    # print(f\"函数开始执行，参数值：index={index}, read={read}, statue={statue}\")\r\n\r\n    try:\r\n        data = load_recent()\r\n        # print(\"加载的数据：\", data)\r\n\r\n        for item in data:\r\n            if item['index'] == index:\r\n                # print(f\"找到匹配项：{item}\")\r\n                if read is not None:\r\n                    # print(f\"更新read从{item['read']}到{read}\")\r\n                    item['read'] = read\r\n                if statue is not None:\r\n                    # print(f\"更新statue从{item['statue']}到{statue}\")\r\n                    item['statue'] = statue\r\n                # print(f\"更新后的项：{item}\")\r\n                break\r\n\r\n        write_json_file('recent.json', data)\r\n        return True\r\n    except ConfigError as e:\r\n        print(f\"Error updating file status: {str(e)}\")\r\n        return False\r\n\r\n\r\ndef delete_entry(index: int) -> bool:\r\n    \"\"\"\r\n    删除指定索引的记录及对应的文件，支持不同的文件扩展名\r\n\r\n    Args:\r\n        index: 要删除的记录索引\r\n\r\n    Returns:\r\n        bool: 操作是否成功\r\n    \"\"\"\r\n    try:\r\n        data = load_recent()\r\n        if data is None:\r\n            return False\r\n\r\n        # 找到要删除的记录\r\n        target_entry = None\r\n        for item in data:\r\n            if item['index'] == index:\r\n                target_entry = item\r\n                break\r\n\r\n        if target_entry:\r\n            print(f\"找到目标记录: {target_entry}\")\r\n\r\n            # 删除原始文件（保持原始扩展名）\r\n            original_file = os.path.join(APP_DATA_DIR, 'static', 'original', target_entry['name'])\r\n            print(f\"原始文件路径: {original_file}\")\r\n            if os.path.exists(original_file):\r\n                os.remove(original_file)\r\n                print(f\"成功删除原始文件: {original_file}\")\r\n            else:\r\n                print(f\"原始文件不存在: {original_file}\")\r\n\r\n            # 删除翻译后的文件（始终使用.pdf扩展名）\r\n            filename_without_ext = os.path.splitext(target_entry['name'])[0]\r\n            target_file = os.path.join(APP_DATA_DIR, 'static', 'target',\r\n                                       f\"{filename_without_ext}_{target_entry['target_language']}.pdf\")\r\n            print(f\"目标文件路径: {target_file}\")\r\n            if os.path.exists(target_file):\r\n                os.remove(target_file)\r\n                print(f\"成功删除目标文件: {target_file}\")\r\n            else:\r\n                print(f\"目标文件不存在: {target_file}\")\r\n\r\n            # 删除双语对照PDF文件\r\n            merged_file = os.path.join(APP_DATA_DIR, 'static', 'merged_pdf',\r\n                                       f\"{filename_without_ext}_{target_entry.get('original_language', 'unknown')}_{target_entry['target_language']}.pdf\")\r\n            print(f\"双语对照文件路径: {merged_file}\")\r\n            if os.path.exists(merged_file):\r\n                os.remove(merged_file)\r\n                print(f\"成功删除双语对照文件: {merged_file}\")\r\n            else:\r\n                print(f\"双语对照文件不存在: {merged_file}\")\r\n\r\n        # 从数据中删除记录\r\n        data = [item for item in data if item['index'] != index]\r\n        write_json_file('recent.json', data)\r\n        return True\r\n    except Exception as e:\r\n        print(f\"Error deleting entry: {str(e)}\")\r\n        return False\r\n\r\n\r\ndef decrease_count() -> bool:\r\n    \"\"\"\r\n    减少计数器值\r\n\r\n    Returns:\r\n        bool: 操作是否成功\r\n    \"\"\"\r\n    try:\r\n        config = load_config()\r\n        if config is None:\r\n            return False\r\n\r\n        config[\"count\"] -= 1\r\n        write_json_file('config.json', config)\r\n        return True\r\n    except ConfigError as e:\r\n        print(f\"Error decreasing count: {str(e)}\")\r\n        return False\r\n\r\n\r\ndef update_default_services(translation: Optional[bool] = None,\r\n                            translation_service: Optional[str] = None,\r\n                            ocr_model: Optional[bool] = None) -> bool:\r\n    \"\"\"\r\n    更新默认服务配置\r\n\r\n    Args:\r\n        translation: 是否启用翻译\r\n        translation_service: 翻译服务提供商\r\n        ocr_model: 是否启用OCR模块\r\n\r\n    Returns:\r\n        bool: 操作是否成功\r\n    \"\"\"\r\n    try:\r\n        config = load_config()\r\n        if config is None:\r\n            return False\r\n\r\n        # 只更新提供的参数\r\n        if translation is not None:\r\n            config[\"default_services\"][\"Enable_translation\"] = str(translation).lower() == 'true'\r\n        if translation_service is not None:\r\n            config[\"default_services\"][\"Translation_api\"] = translation_service\r\n        if ocr_model is not None:\r\n            config[\"default_services\"][\"ocr_model\"] = str(ocr_model).lower() == 'true'\r\n\r\n        write_json_file('config.json', config)\r\n        return True\r\n    except ConfigError as e:\r\n        print(f\"Error updating default services: {str(e)}\")\r\n        return False\r\n\r\n\r\ndef get_default_services() -> Optional[Dict]:\r\n    \"\"\"\r\n    获取默认服务配置值，确保每次都读取最新配置\r\n\r\n    Returns:\r\n        Dict: 包含default_services的所有配置值的字典,格式为:\r\n        {\r\n            \"translation\": bool,\r\n            \"translation_service\": str,\r\n            \"ocr_model\": bool\r\n        }\r\n        如果获取失败则返回None\r\n    \"\"\"\r\n    try:\r\n        # 强制重新加载配置以确保获取最新设置\r\n        config = load_config(force_reload=True)\r\n        if config is None:\r\n            return None\r\n\r\n        return {\r\n            \"translation\": config[\"default_services\"][\"Enable_translation\"],\r\n            \"translation_service\": config[\"default_services\"][\"Translation_api\"],\r\n            \"ocr_model\": config[\"default_services\"][\"ocr_model\"],\r\n            \"count\": config[\"count\"],\r\n        }\r\n    except ConfigError as e:\r\n        print(f\"Error getting default services: {str(e)}\")\r\n        return None\r\n\r\n\r\ndef save_config(config):\r\n    \"\"\"\r\n    保存配置文件\r\n\r\n    Args:\r\n        config: 要保存的配置数据\r\n\r\n    Returns:\r\n        bool: 操作是否成功\r\n    \"\"\"\r\n    # 创建备份\r\n    CONFIG_FILE = os.path.join(APP_DATA_DIR, 'config.json')\r\n    if os.path.exists(CONFIG_FILE):\r\n        backup_file = f\"{CONFIG_FILE}.bak\"\r\n        try:\r\n            with open(CONFIG_FILE, 'r', encoding='utf-8') as f:\r\n                original_content = f.read()\r\n            with open(backup_file, 'w', encoding='utf-8') as f:\r\n                f.write(original_content)\r\n        except Exception as e:\r\n            print(f\"创建备份文件失败: {e}\")\r\n\r\n    # 保存新配置\r\n    try:\r\n        with open(CONFIG_FILE, 'w', encoding='utf-8') as f:\r\n            json.dump(config, f, indent=4, ensure_ascii=False)\r\n        return True\r\n    except Exception as e:\r\n        print(f\"保存配置文件失败: {e}\")\r\n        # 如果保存失败且存在备份，尝试恢复\r\n        if os.path.exists(f\"{CONFIG_FILE}.bak\"):\r\n            try:\r\n                with open(f\"{CONFIG_FILE}.bak\", 'r', encoding='utf-8') as f:\r\n                    backup_content = f.read()\r\n                with open(CONFIG_FILE, 'w', encoding='utf-8') as f:\r\n                    f.write(backup_content)\r\n            except Exception as restore_error:\r\n                print(f\"恢复备份失败: {restore_error}\")\r\n        return False\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    # 确保目录存在\r\n    os.makedirs(os.path.join(APP_DATA_DIR, 'static', 'original'), exist_ok=True)\r\n    os.makedirs(os.path.join(APP_DATA_DIR, 'static', 'target'), exist_ok=True)\r\n\r\n    # 创建测试用的 recent.json\r\n    test_data = [\r\n        {\r\n            \"index\": 1,\r\n            \"name\": \"g2.epub\",  # 注意这里是.epub\r\n            \"target_language\": \"zh\",\r\n            \"status\": \"completed\",\r\n            \"timestamp\": \"2024-01-20 12:00:00\"\r\n        }\r\n    ]\r\n    write_json_file('recent.json', test_data)\r\n\r\n    # 在删除之前先检查文件是否存在\r\n    print(\"检查初始状态...\")\r\n    print(f\"应用数据目录: {APP_DATA_DIR}\")\r\n    print(f\"原始文件(.epub)是否存在: {os.path.exists(os.path.join(APP_DATA_DIR, 'static', 'original', 'g2.epub'))}\")\r\n    print(f\"目标文件(.pdf)是否存在: {os.path.exists(os.path.join(APP_DATA_DIR, 'static', 'target', 'g2_zh.pdf'))}\")\r\n\r\n    print(\"\\n开始测试删除功能...\")\r\n    result = delete_entry(1)\r\n    print(f\"删除操作结果: {result}\")\r\n\r\n    print(\"\\n检查最终状态...\")\r\n    print(f\"原始文件(.epub)是否存在: {os.path.exists(os.path.join(APP_DATA_DIR, 'static', 'original', 'g2.epub'))}\")\r\n    print(f\"目标文件(.pdf)是否存在: {os.path.exists(os.path.join(APP_DATA_DIR, 'static', 'target', 'g2_zh.pdf'))}\")"
  },
  {
    "path": "main.py",
    "content": "import All_Translation as at\r\nfrom PIL import Image\r\nimport pytesseract\r\nimport time\r\nimport fitz\r\nimport os\r\n\r\nimport load_config\r\nimport update_recent  # 导入update_recent模块\r\n\r\nfrom datetime import datetime\r\nimport pdf_thumbnail\r\nfrom load_config import APP_DATA_DIR\r\nimport get_new_blocks as new_blocks\r\nimport Subset_Font\r\nimport merge_pdf\r\n\r\ndef get_current_config():\r\n    \"\"\"获取当前最新配置\"\"\"\r\n    return load_config.load_config(force_reload=True)\r\n\r\ndef decimal_to_hex_color(decimal_color):\r\n    if decimal_color == 0:\r\n        return '#000000'  # 黑色\r\n\r\n    # 将十进制数转换为十六进制，并移除'0x'前缀\r\n    hex_color = hex(decimal_color)[2:]\r\n\r\n    # 确保是6位十六进制数\r\n    hex_color = hex_color.zfill(6)\r\n\r\n    # 添加'#'前缀\r\n    return f'#{hex_color}'\r\n\r\ndef is_math(text, page_num,font_info):\r\n    return False\r\n\r\ndef line_non_text(text):\r\n    return True\r\n\r\n\r\ndef is_non_text(text):\r\n    return False\r\n\r\nclass main_function:\r\n    def __init__(self, pdf_path,\r\n                 original_language, target_language,bn = None,en = None,\r\n                 DPI=72,):\r\n        \"\"\"\r\n        这里的参数与原来保持一致或自定义。主要多加一个 self.pages_data 用于存储所有页面的提取结果。\r\n        \"\"\"\r\n\r\n        self.pdf_path = pdf_path\r\n        self.full_path = os.path.join(APP_DATA_DIR, 'static', 'original', pdf_path)\r\n        self.doc = fitz.open(self.full_path)\r\n\r\n        self.original_language = original_language\r\n        self.target_language = target_language\r\n        self.DPI = DPI\r\n        \r\n        # 动态获取配置，不再在初始化时缓存\r\n        config = get_current_config()\r\n        self.translation = config['default_services']['Enable_translation']\r\n        self.translation_type = config['default_services']['Translation_api']\r\n        self.use_mupdf = not config['default_services']['ocr_model']\r\n        self.PPC = config['PPC']  # 将PPC作为实例变量\r\n        \r\n        self.bn = bn\r\n        self.en = en\r\n\r\n        # 初始化字体计数器\r\n        self.font_usage_counter = {\"normal\": 0, \"bold\": 0}\r\n        self.font_embed_counter = {\"normal\": 0, \"bold\": 0}\r\n        self.font_css_cache = {}\r\n\r\n        self.t = time.time()\r\n        # 新增一个全局列表，用于存所有页面的 [文本, bbox]，以及翻译后结果\r\n        # 形式: self.pages_data[page_index] = [ [原文, bbox], [原文, bbox], ... ]\r\n        self.pages_data = []\r\n\r\n    def main(self):\r\n        \"\"\"\r\n        主流程函数。只做“计数更新、生成缩略图、建条目”等老逻辑，替换原来在这里的逐页翻译写入。\r\n        但是保留 if use_mupdf: for... self.start(...) else: for... self.start(...)\r\n        不做“翻译和写入”的动作，而是只做“提取文本”。\r\n        提取完所有页面后，批量翻译，再统一写入 PDF。\r\n        \"\"\"\r\n        # 1. 计数和配置信息\r\n        load_config.update_count()\r\n        config = get_current_config()  # 获取最新配置\r\n        count = config[\"count\"]\r\n\r\n\r\n        # 2. 生成 PDF 缩略图 (保留原逻辑)\r\n        pdf_thumbnail.create_pdf_thumbnail(self.full_path, width=400)\r\n\r\n        # 3. 创建新条目（保留原逻辑）\r\n        current_time = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\r\n        new_entry = {\r\n            \"index\": count,\r\n            \"date\": current_time,\r\n            \"name\": self.pdf_path,\r\n            \"original_language\": self.original_language,\r\n            \"target_language\": self.target_language,\r\n            \"read\": \"0\",\r\n            \"statue\": \"0\"\r\n        }\r\n        load_config.add_new_entry(new_entry)\r\n\r\n        # 4. 保留原先判断是否 use_mupdf 的代码，以便先提取文本\r\n        page_count =self.doc.page_count\r\n        if self.bn == None:\r\n            self.bn = 0\r\n        if self.en == None:\r\n            self.en = page_count\r\n\r\n        if self.use_mupdf:\r\n            start_page = self.bn\r\n            end_page = min(self.en, page_count)\r\n\r\n            # 使用 PyMuPDF 直接获取文本块\r\n            for i in range(start_page, end_page):\r\n                self.start(image=None, pag_num=i)  # 只做提取，不做翻译写入\r\n        else:\r\n            # OCR 模式\r\n            zoom = self.DPI / 72\r\n            mat = fitz.Matrix(zoom, zoom)\r\n            # 处理从 self.bn 到 self.en 的页面范围，并确保 self.en 不超过文档页数\r\n            start_page = self.bn\r\n            end_page = min(self.en, page_count)\r\n\r\n            # 迭代指定范围的页面\r\n            for i in range(start_page, end_page):\r\n                page = self.doc[i]  # 获取指定页面\r\n                pix = page.get_pixmap(matrix=mat)\r\n                image = Image.frombytes(\"RGB\", [pix.width, pix.height], pix.samples)\r\n                # 如果需要保存图像到文件，可自行保留或注释\r\n                # image.save(f'page_{i}.jpg', 'JPEG')\r\n                self.start(image=image, pag_num=i)  # 只做提取，不做翻译写入\r\n\r\n        # 5. 若开启翻译，则批量翻译所有提取的文本\r\n        # 使用实例变量 self.PPC 而不是全局变量\r\n        self.batch_translate_pages_data(\r\n                original_language=self.original_language,\r\n                target_language=self.target_language,\r\n                translation_type=self.translation_type,\r\n                batch_size=self.PPC\r\n            )\r\n        # 6. 子集化字体\r\n        bold_text = \"\"\r\n        normal_text = \"\"\r\n\r\n        # 遍历所有页码\r\n        for page in self.pages_data:\r\n            for item in page:\r\n                text = item[0]  # 原始文本\r\n                translate_text = item[2]  # 翻译文本\r\n                is_bold = item[6]  # text_bold值\r\n\r\n                if is_bold:\r\n                    bold_text += translate_text   # 在每个文本之间添加空格\r\n                else:\r\n                    normal_text += translate_text\r\n\r\n        # 去除末尾多余的空格\r\n        bold_text = bold_text.strip()\r\n        normal_text = normal_text.strip()\r\n\r\n        # 打印结果\r\n        if bold_text:  # 如果粗体文本不为空\r\n            in_font_path = os.path.join(APP_DATA_DIR, 'temp', 'fonts', f\"{self.target_language}_bold.ttf\")\r\n            out_font_path = os.path.join(APP_DATA_DIR, 'temp', 'fonts', f\"{self.target_language}_bold_subset.ttf\")\r\n            self.subset_font(in_font_path=in_font_path, out_font_path=out_font_path, text=bold_text)\r\n\r\n            in_font_path2 = os.path.join(APP_DATA_DIR, 'temp', 'fonts', f\"{self.target_language}.ttf\")\r\n            out_font_path2 = os.path.join(APP_DATA_DIR, 'temp', 'fonts', f\"{self.target_language}_subset.ttf\")\r\n            self.subset_font(in_font_path=in_font_path2, out_font_path=out_font_path2, text=normal_text)\r\n\r\n        else:\r\n            in_font_path = os.path.join(APP_DATA_DIR, 'temp', 'fonts', f\"{self.target_language}.ttf\")\r\n            out_font_path = os.path.join(APP_DATA_DIR, 'temp', 'fonts', f\"{self.target_language}_subset.ttf\")\r\n            self.subset_font(in_font_path=in_font_path, out_font_path=out_font_path, text=normal_text)\r\n\r\n\r\n\r\n        # 7. 将翻译结果统一写入 PDF（覆盖+插入译文）\r\n        self.apply_translations_to_pdf()\r\n\r\n        # 8. 保存 PDF、更新状态\r\n        pdf_name, _ = os.path.splitext(self.pdf_path)\r\n        target_path = os.path.join(APP_DATA_DIR, 'static', 'target', f\"{pdf_name}_{self.target_language}.pdf\")\r\n        \r\n        print(\"正在保存PDF文件,耐心等待...\")\r\n        # 创建新文档并从当前文档复制内容以避免字体重复问题\r\n        new_doc = fitz.open()\r\n        new_doc.insert_pdf(self.doc)\r\n        new_doc.save(target_path, garbage=4, deflate=True)\r\n        new_doc.close()\r\n\r\n        load_config.update_file_status(count, statue=\"1\")  # statue = \"1\"\r\n\r\n        # 打印总耗时\r\n        end_time = time.time()\r\n        total_duration = end_time - self.t\r\n        \r\n        print(f\"翻译共耗时: {total_duration:.2f}秒\")\r\n        \r\n        merged_output_path = os.path.join(APP_DATA_DIR, 'static', 'merged_pdf', f\"{pdf_name}_{self.original_language}_{self.target_language}.pdf\")\r\n\r\n        print(\"正在创建双语对照PDF...\")\r\n        merge_pdf.merge_pdfs_horizontally(pdf1_path=self.full_path,pdf2_path=target_path,output_path=merged_output_path)\r\n        print(f\"处理完成！输出文件: {target_path}\")\r\n        \r\n        # 更新recent.json文件\r\n        print(\"正在更新最近处理记录...\")\r\n        update_recent.update_recent_json()\r\n        print(\"记录更新完成！\")\r\n\r\n    def start(self, image, pag_num):\r\n        \"\"\"\r\n        原先逐页处理的函数，现仅负责“提取文本并存储在 self.pages_data[pag_num]”。\r\n        不在这里直接翻译或写回 PDF。\r\n        \"\"\"\r\n        # 确保 self.pages_data 有 pag_num 对应的列表\r\n        while len(self.pages_data) <= pag_num:\r\n            self.pages_data.append([])  # 每个元素是 [ [text, (x0,y0,x1,y1)], ... ]\r\n\r\n        page = self.doc.load_page(pag_num)\r\n\r\n        if self.use_mupdf and image is None:\r\n            blocks = new_blocks.get_new_blocks(page)\r\n            # 如果获取到的 blocks 为空，则进行相应处理\r\n            if not blocks:\r\n                return True\r\n\r\n\r\n\r\n            for block in blocks:\r\n                text_type = block[2]  # 类型\r\n                if text_type == 'math':\r\n                    continue\r\n                else:\r\n                    text = block[0]  # 文本内容\r\n                    text_bbox = block[1]  # 边界框坐标\r\n                    text_angle = block[3]\r\n                    text_color = block[4]\r\n                    text_indent = block[5]\r\n                    text_bold = block[6]\r\n                    text_size = block[7]\r\n                    # 转换颜色值\r\n                    html_color = decimal_to_hex_color(text_color)\r\n                    self.pages_data[pag_num].append(\r\n                        [text, tuple(text_bbox), None, text_angle,html_color,text_indent,text_bold,text_size])\r\n\r\n\r\n        else:\r\n            # OCR 提取文字\r\n            config = load_config.load_config()\r\n            tesseract_path = config['ocr_services']['tesseract']['path']\r\n            pytesseract.pytesseract.tesseract_cmd = tesseract_path\r\n\r\n            Full_width, Full_height = image.size\r\n            ocr_result = pytesseract.image_to_data(image, output_type=pytesseract.Output.DICT)\r\n\r\n            current_paragraph_text = ''\r\n            paragraph_bbox = {\r\n                'left': float('inf'),\r\n                'top': float('inf'),\r\n                'right': 0,\r\n                'bottom': 0\r\n            }\r\n            current_block_num = None\r\n            Threshold_width = 0.06 * Full_width\r\n            Threshold_height = 0.006 * Full_height\r\n\r\n            for i in range(len(ocr_result['text'])):\r\n                block_num = ocr_result['block_num'][i]\r\n                text_ocr = ocr_result['text'][i].strip()\r\n                left = ocr_result['left'][i]\r\n                top = ocr_result['top'][i]\r\n                width = ocr_result['width'][i]\r\n                height = ocr_result['height'][i]\r\n\r\n                if text_ocr and not is_math(text_ocr, pag_num, font_info='22') and not is_non_text(text_ocr):\r\n\r\n                    # 若换 block 或段落间隔较大，则保存上一段\r\n                    if (block_num != current_block_num or\r\n                       (abs(left - paragraph_bbox['right']) > Threshold_width and\r\n                        abs(height - (paragraph_bbox['bottom'] - paragraph_bbox['top'])) > Threshold_height and\r\n                        abs(left - paragraph_bbox['left']) > Threshold_width)):\r\n\r\n                        if current_paragraph_text:\r\n                            # 转换到 PDF 坐标\r\n                            Full_rect = page.rect\r\n                            w_points = Full_rect.width\r\n                            h_points = Full_rect.height\r\n\r\n                            x0_ratio = paragraph_bbox['left'] / Full_width\r\n                            y0_ratio = paragraph_bbox['top'] / Full_height\r\n                            x1_ratio = paragraph_bbox['right'] / Full_width\r\n                            y1_ratio = paragraph_bbox['bottom'] / Full_height\r\n\r\n                            x0_pdf = x0_ratio * w_points\r\n                            y0_pdf = y0_ratio * h_points\r\n                            x1_pdf = x1_ratio * w_points\r\n                            y1_pdf = y1_ratio * h_points\r\n\r\n                            self.pages_data[pag_num].append([\r\n                                current_paragraph_text.strip(),\r\n                                (x0_pdf, y0_pdf, x1_pdf, y1_pdf)\r\n                            ])\r\n\r\n                        # 重置\r\n                        current_paragraph_text = ''\r\n                        paragraph_bbox = {\r\n                            'left': float('inf'),\r\n                            'top': float('inf'),\r\n                            'right': 0,\r\n                            'bottom': 0\r\n                        }\r\n                        current_block_num = block_num\r\n\r\n                    # 继续累加文本\r\n                    current_paragraph_text += text_ocr + \" \"\r\n                    paragraph_bbox['left'] = min(paragraph_bbox['left'], left)\r\n                    paragraph_bbox['top'] = min(paragraph_bbox['top'], top)\r\n                    paragraph_bbox['right'] = max(paragraph_bbox['right'], left + width)\r\n                    paragraph_bbox['bottom'] = max(paragraph_bbox['bottom'], top + height)\r\n\r\n            # 收尾：最后一段存入\r\n            if current_paragraph_text:\r\n                Full_rect = page.rect\r\n                w_points = Full_rect.width\r\n                h_points = Full_rect.height\r\n\r\n                x0_ratio = paragraph_bbox['left'] / Full_width\r\n                y0_ratio = paragraph_bbox['top'] / Full_height\r\n                x1_ratio = paragraph_bbox['right'] / Full_width\r\n                y1_ratio = paragraph_bbox['bottom'] / Full_height\r\n\r\n                x0_pdf = x0_ratio * w_points\r\n                y0_pdf = y0_ratio * h_points\r\n                x1_pdf = x1_ratio * w_points\r\n                y1_pdf = y1_ratio * h_points\r\n\r\n                self.pages_data[pag_num].append([\r\n                    current_paragraph_text.strip(),\r\n                    (x0_pdf, y0_pdf, x1_pdf, y1_pdf),\r\n                    None\r\n                ])\r\n\r\n        # 注意：这里不做翻译、不插入 PDF，只负责“收集文本”到 self.pages_data\r\n\r\n    def batch_translate_pages_data(self, original_language, target_language,\r\n                                   translation_type, batch_size):\r\n        \"\"\"PPC (Pages Per Call)\r\n        分批翻译 pages_data，每次处理最多 batch_size 页的文本，避免一次性过多。\r\n        将译文存回 self.pages_data 的第三个元素，如 [原文, bbox, 译文]\r\n        \"\"\"\r\n        # 重新获取最新配置确保翻译设置是最新的\r\n        config = get_current_config()\r\n        use_mupdf = not config['default_services']['ocr_model']\r\n        \r\n        total_pages = len(self.pages_data)\r\n        start_idx = 0\r\n\r\n        while start_idx < total_pages:\r\n            end_idx = min(start_idx + batch_size, total_pages)\r\n\r\n            # 收集该批次的所有文本\r\n            batch_texts = []\r\n            for i in range(start_idx, end_idx):\r\n                for block in self.pages_data[i]:\r\n                    batch_texts.append(block[0])  # block[0] = 原文\r\n\r\n            # 翻译\r\n\r\n\r\n            if self.translation and use_mupdf:\r\n                translation_list = at.Online_translation(\r\n                    original_language=original_language,\r\n                    target_language=target_language,\r\n                    translation_type=translation_type,\r\n                    texts_to_process=batch_texts\r\n                ).translation()\r\n\r\n            else:\r\n                translation_list = batch_texts\r\n\r\n\r\n\r\n            # 回填译文\r\n            idx_t = 0\r\n            for i in range(start_idx, end_idx):\r\n                for block in self.pages_data[i]:\r\n                    # 在第三个位置添加翻译文本\r\n                    block[2] = translation_list[idx_t]\r\n                    idx_t += 1\r\n\r\n            start_idx += batch_size\r\n            print('当前进度',end_idx,\"/\",total_pages)\r\n\r\n\r\n\r\n    def apply_translations_to_pdf(self):\r\n        \"\"\"\r\n        统一对 PDF 做\"打码/打白 + 插入译文\"操作\r\n        \"\"\"\r\n        start_time = time.time()\r\n        \r\n        for page_index, blocks in enumerate(self.pages_data):\r\n            page = self.doc.load_page(page_index)\r\n            \r\n            # 按字体类型分组该页面的文本块，避免重复定义字体\r\n            normal_blocks = []\r\n            bold_blocks = []\r\n            \r\n            # 先覆盖所有区域\r\n            for block in blocks:\r\n                coords = block[1]  # (x0, y0, x1, y1)\r\n                \r\n                # 智能计算扩展比例，根据翻译文本和原文的长度比来决定\r\n                original_text = block[0]\r\n                translated_text = block[2] if block[2] is not None else original_text\r\n                \r\n                # 计算扩展因子：最大限制为5%的扩展\r\n                len_ratio = min(1.05, max(1.01, len(translated_text) / max(1, len(original_text))))\r\n                \r\n                x0, y0, x1, y1 = coords\r\n                width = x1 - x0\r\n                height = y1 - y0\r\n                \r\n                # 只向右侧扩展，不改变左侧起点\r\n                h_expand = (len_ratio - 1) * width\r\n                \r\n                # 应用扩展，只修改x1值\r\n                x1 = x1 + h_expand\r\n                \r\n                # 缩小上下方向的覆盖区域，使其更加紧凑\r\n                # 计算上下边距缩小量，但保留一个最小边距\r\n                vertical_margin = min(height * 0.1, 3)  # 上下各缩小10%，但最多3个点\r\n                \r\n                # 应用上下缩小\r\n                y0 = y0 + vertical_margin\r\n                y1 = y1 - vertical_margin\r\n                \r\n                # 确保最小高度\r\n                if y1 - y0 < 10:  # 保证最小高度为10pt\r\n                    y_center = (coords[1] + coords[3]) / 2  # 使用原始bbox的中心点\r\n                    y0 = y_center - 5\r\n                    y1 = y_center + 5\r\n                \r\n                enlarged_coords = (x0, y0, x1, y1)\r\n                rect = fitz.Rect(*enlarged_coords)\r\n\r\n                # 先尝试使用 Redact 遮盖\r\n                try:\r\n                    page.add_redact_annot(rect)\r\n                    page.apply_redactions(images=fitz.PDF_REDACT_IMAGE_NONE)\r\n                except Exception as e:\r\n                    # 若 Redact 失败，改用白色方块覆盖\r\n                    annots = list(page.annots() or [])\r\n                    if annots:\r\n                        page.delete_annot(annots[-1])\r\n                    try:\r\n                        page.draw_rect(rect, color=(1, 1, 1), fill=(1, 1, 1))\r\n                    except Exception as e2:\r\n                        print(f\"创建白色画布时发生错误: {e2}\")\r\n                    print(f\"应用重编辑时发生错误: {e}\")\r\n                \r\n                # 分类文本块\r\n                if len(block) > 6 and block[6]:  # text_bold\r\n                    bold_blocks.append((block, enlarged_coords))\r\n                else:\r\n                    normal_blocks.append((block, enlarged_coords))\r\n            \r\n            # 处理普通字体文本块\r\n            if normal_blocks:\r\n                font_family = f\"{self.target_language}_font\"\r\n                font_path = os.path.join(APP_DATA_DIR, 'temp', 'fonts', f\"{self.target_language}_subset.ttf\")\r\n                font_path = font_path.replace('\\\\', '/')\r\n                \r\n                # 确保字体文件存在\r\n                if not os.path.exists(font_path):\r\n                    print(f\"警告：字体文件不存在: {font_path}\")\r\n                \r\n                # 更新字体使用计数\r\n                self.font_usage_counter[\"normal\"] += len(normal_blocks)\r\n                \r\n                # 只有第一次使用该字体时添加@font-face定义\r\n                if font_family not in self.font_css_cache:\r\n                    css_prefix = f\"\"\"\r\n                    @font-face {{\r\n                        font-family: \"{font_family}\";\r\n                        src: url(\"{font_path}\");\r\n                    }}\r\n                    \"\"\"\r\n                    self.font_css_cache[font_family] = css_prefix\r\n                    self.font_embed_counter[\"normal\"] += 1\r\n                else:\r\n                    css_prefix = self.font_css_cache[font_family]\r\n                \r\n                # 处理每个普通字体文本块\r\n                for block_data in normal_blocks:\r\n                    block, enlarged_coords = block_data\r\n                    # 如果第三个元素是译文，则用之，否则用原文\r\n                    translated_text = block[2] if block[2] is not None else block[0]\r\n                    angle = block[3] if len(block) > 3 else 0\r\n                    html_color = block[4] if len(block) > 4 else '#000000'\r\n                    text_indent = block[5] if len(block) > 5 else 0\r\n                    text_size = float(block[7]) if len(block) > 7 else 12\r\n                    \r\n                    # 使用扩大后的坐标创建矩形\r\n                    rect = fitz.Rect(*enlarged_coords)\r\n                    \r\n                    # 组合CSS，添加自动调整大小和自动换行属性\r\n                    css = css_prefix + f\"\"\"\r\n                    * {{\r\n                        font-family: \"{font_family}\";\r\n                        color: {html_color};\r\n                        text-indent: {text_indent}pt;  \r\n                        font-size: {text_size}pt; \r\n                        line-height: 1.5;\r\n                        word-wrap: break-word;\r\n                        overflow-wrap: break-word;\r\n                        width: 100%;\r\n                        box-sizing: border-box;\r\n                    }}\r\n                    \"\"\"\r\n                    \r\n                    # 插入文本\r\n                    page.insert_htmlbox(\r\n                        rect,\r\n                        translated_text,\r\n                        css=css,\r\n                        rotate=angle\r\n                    )\r\n            \r\n            # 处理粗体字体文本块\r\n            if bold_blocks:\r\n                font_family = f\"{self.target_language}_bold_font\"\r\n                font_path = os.path.join(APP_DATA_DIR, 'temp', 'fonts', f\"{self.target_language}_bold_subset.ttf\")\r\n                font_path = font_path.replace('\\\\', '/')\r\n                \r\n                # 确保字体文件存在\r\n                if not os.path.exists(font_path):\r\n                    print(f\"警告：字体文件不存在: {font_path}\")\r\n                \r\n                # 更新字体使用计数\r\n                self.font_usage_counter[\"bold\"] += len(bold_blocks)\r\n                \r\n                # 只有第一次使用该字体时添加@font-face定义\r\n                if font_family not in self.font_css_cache:\r\n                    css_prefix = f\"\"\"\r\n                    @font-face {{\r\n                        font-family: \"{font_family}\";\r\n                        src: url(\"{font_path}\");\r\n                    }}\r\n                    \"\"\"\r\n                    self.font_css_cache[font_family] = css_prefix\r\n                    self.font_embed_counter[\"bold\"] += 1\r\n                else:\r\n                    css_prefix = self.font_css_cache[font_family]\r\n                \r\n                # 处理每个粗体字体文本块\r\n                for block_data in bold_blocks:\r\n                    block, enlarged_coords = block_data\r\n                    # 如果第三个元素是译文，则用之，否则用原文\r\n                    translated_text = block[2] if block[2] is not None else block[0]\r\n                    angle = block[3] if len(block) > 3 else 0\r\n                    html_color = block[4] if len(block) > 4 else '#000000'\r\n                    text_indent = block[5] if len(block) > 5 else 0\r\n                    text_size = float(block[7])  if len(block) > 7 else 12\r\n                    \r\n                    # 使用扩大后的坐标创建矩形\r\n                    rect = fitz.Rect(*enlarged_coords)\r\n                    \r\n                    # 组合CSS，添加自动调整大小和自动换行属性\r\n                    css = css_prefix + f\"\"\"\r\n                    * {{\r\n                        font-family: \"{font_family}\";\r\n                        color: {html_color};\r\n                        text-indent: {text_indent}pt;  \r\n                        font-size: {text_size}pt;\r\n                        line-height: 1.2;\r\n                        word-wrap: break-word;\r\n                        overflow-wrap: break-word;\r\n                        width: 100%;\r\n                        box-sizing: border-box;\r\n                    }}\r\n                    \"\"\"\r\n                    \r\n                    # 插入文本\r\n                    page.insert_htmlbox(\r\n                        rect,\r\n                        translated_text,\r\n                        css=css,\r\n                        rotate=angle\r\n                    )\r\n            \r\n            # 每20页打印一次简单进度\r\n            if page_index % 20 == 0:\r\n                print(f\"正在处理: {page_index}/{len(self.pages_data)} 页\")\r\n        \r\n        # 处理完全部页面后显示结束信息\r\n        total_time = time.time() - start_time\r\n        print(f\"文本插入完成，用时 {total_time:.2f} 秒\")\r\n\r\n    def subset_font(self, in_font_path, out_font_path,text):\r\n        Subset_Font.subset_font(in_font_path=in_font_path,out_font_path=out_font_path,text=text,language=self.target_language)\r\n\r\nif __name__ == '__main__':\r\n    config = get_current_config()\r\n    main_function(original_language='auto', target_language='zh', pdf_path='g6.pdf').main()\r\n"
  },
  {
    "path": "merge_pdf.py",
    "content": "import fitz\nimport os\n\ndef merge_pdfs_horizontally(pdf1_path, pdf2_path, output_path, spacing=0):\n    \"\"\"\n    水平合并两个PDF文件的所有页面\n    :param pdf1_path: 第一个PDF文件的绝对路径\n    :param pdf2_path: 第二个PDF文件的绝对路径\n    :param output_path: 输出PDF文件的绝对路径\n    :param spacing: 两个PDF之间的间距（点）\n    \"\"\"\n    # 确保输入路径存在\n    if not os.path.exists(pdf1_path):\n        raise FileNotFoundError(f\"找不到第一个PDF文件: {pdf1_path}\")\n    if not os.path.exists(pdf2_path):\n        raise FileNotFoundError(f\"找不到第二个PDF文件: {pdf2_path}\")\n\n    # 打开两个源PDF文件\n    doc1 = fitz.open(pdf1_path)\n    doc2 = fitz.open(pdf2_path)\n\n    # 创建新的PDF文档\n    result_doc = fitz.open()\n\n    # 确保两个文档都至少有一页\n    if doc1.page_count == 0 or doc2.page_count == 0:\n        raise ValueError(\"Both PDFs must have at least one page\")\n\n    # 确保两个PDF的页数相同\n    if doc1.page_count != doc2.page_count:\n        raise ValueError(\"Both PDFs must have the same number of pages\")\n\n    # 处理每一页\n    for page_num in range(doc1.page_count):\n        # 获取两个PDF的当前页\n        page1 = doc1[page_num]\n        page2 = doc2[page_num]\n\n        # 获取页面尺寸\n        rect1 = page1.rect\n        rect2 = page2.rect\n\n        # 计算新页面的尺寸\n        new_width = rect1.width + rect2.width + spacing\n        new_height = max(rect1.height, rect2.height)\n\n        # 创建新页面\n        new_page = result_doc.new_page(width=new_width, height=new_height)\n\n        # 创建第一个PDF的位置矩阵（保持在左侧）\n        matrix1 = fitz.Matrix(1, 1)\n\n        # 创建第二个PDF的位置矩阵（移动到右侧）\n        matrix2 = fitz.Matrix(1, 1)\n        x_shift = rect1.width + spacing\n        matrix2.pretranslate(x_shift, 0)\n\n        # 将两个页面内容复制到新页面\n        new_page.show_pdf_page(rect1, doc1, page_num, matrix1)\n        new_page.show_pdf_page(fitz.Rect(x_shift, 0, x_shift + rect2.width, new_height),\n                               doc2, page_num, matrix2)\n\n    # 确保输出目录存在\n    output_dir = os.path.dirname(output_path)\n    if not os.path.exists(output_dir):\n        os.makedirs(output_dir)\n\n    # 保存结果\n    result_doc.save(output_path)\n\n    # 关闭所有文档\n    doc1.close()\n    doc2.close()\n    result_doc.close()\n\n# 使用示例\nif __name__ == \"__main__\":\n    pdf1_path = r\"g6.pdf\"\n    pdf2_path = r\"g6_zh.pdf\"\n    output_path = r\"./output/merged.pdf\"\n\n    try:\n        merge_pdfs_horizontally(pdf1_path, pdf2_path, output_path)\n        print(\"PDFs merged successfully!\")\n        print(f\"Output saved to: {output_path}\")\n    except FileNotFoundError as e:\n        print(f\"File error: {str(e)}\")\n    except Exception as e:\n        print(f\"Error occurred: {str(e)}\")\n"
  },
  {
    "path": "pdf_thumbnail.py",
    "content": "import fitz\nimport os\n\n\ndef create_pdf_thumbnail(pdf_path, width=400):\n    \"\"\"\n    为PDF文件第一页创建缩略图并保存到pdf_path上一层目录的thumbnail文件夹\n\n    参数:\n        pdf_path: PDF文件路径\n        width: 缩略图的宽度（像素）\n    \"\"\"\n    try:\n        # 获取PDF文件名（不含扩展名）\n        pdf_filename = os.path.splitext(os.path.basename(pdf_path))[0]\n\n        # 获取PDF文件的绝对路径\n        pdf_absolute_path = os.path.abspath(pdf_path)\n\n        # 获取PDF文件所在目录的上一层目录\n        parent_dir = os.path.dirname(os.path.dirname(pdf_absolute_path))\n\n        # 构建保存缩略图的路径（上一层目录的thumbnail文件夹）\n        thumbnail_dir = os.path.join(parent_dir, 'thumbnail')\n\n        # 如果目录不存在，创建目录\n        os.makedirs(thumbnail_dir, exist_ok=True)\n\n        # 构建输出路径\n        output_path = os.path.join(thumbnail_dir, f\"{pdf_filename}.png\")\n\n        # 打开PDF文件\n        doc = fitz.open(pdf_path)\n\n        # 获取第一页\n        first_page = doc[0]\n\n        # 设置缩放参数\n        zoom = width / first_page.rect.width\n        matrix = fitz.Matrix(zoom, zoom)\n\n        # 获取页面的像素图\n        pix = first_page.get_pixmap(matrix=matrix, alpha=False)\n\n        # 保存图片\n        pix.save(output_path)\n\n        # 关闭PDF文档\n        doc.close()\n\n        print(f\"缩略图已保存到: {output_path}\")\n        return output_path\n\n    except Exception as e:\n        print(f\"生成缩略图时发生错误: {str(e)}\")\n        return None\n\n\n# 使用示例\nif __name__ == \"__main__\":\n    # PDF文件路径\n    pdf_file = \"g55.pdf\"\n    # 生成并保存缩略图\n    thumbnail_path = create_pdf_thumbnail(pdf_file, width=400)\n"
  },
  {
    "path": "pdfviewer.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Enhanced Split PDF Viewer</title>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css\">\n    <style>\n        * {\n            margin: 0;\n            padding: 0;\n            box-sizing: border-box;\n        }\n\n        .container {\n            display: flex;\n            width: 100%;\n            height: 100vh;\n            background-color: #f0f0f0;\n            position: relative;\n            transition: all 0.3s ease;\n        }\n\n        .pdf-container {\n            flex: 1;\n            padding: 10px;\n            display: flex;\n            flex-direction: column;\n            position: relative;\n            transition: all 0.3s ease;\n        }\n\n        .pdf-container.fullscreen {\n            position: fixed;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100vh;\n            z-index: 1000;\n            background-color: #f0f0f0;\n            padding: 20px;\n        }\n\n        .divider {\n            width: 4px;\n            height: 100%;\n            background-color: #666;\n            cursor: col-resize;\n            transition: background-color 0.3s;\n        }\n        \n        .divider:hover {\n            background-color: #999;\n        }\n\n        .pdf-viewer {\n            width: 100%;\n            height: 100%;\n            border: 1px solid #ccc;\n            border-radius: 4px;\n            background-color: white;\n            overflow: hidden;\n            position: relative;\n        }\n\n        .pdf-title {\n            text-align: center;\n            padding: 5px;\n            margin-bottom: 10px;\n            background-color: #333;\n            color: white;\n            border-radius: 4px;\n            display: flex;\n            justify-content: center;\n            align-items: center;\n        }\n\n        embed, object, iframe {\n            width: 100%;\n            height: 100%;\n            border: none;\n        }\n\n        .zoom-controls {\n            position: absolute;\n            top: 50px;\n            right: 20px;\n            z-index: 100;\n            display: flex;\n            flex-direction: column;\n            gap: 10px;\n        }\n\n        .zoom-btn {\n            background-color: rgba(0, 0, 0, 0.7);\n            color: white;\n            border: none;\n            padding: 8px 12px;\n            border-radius: 4px;\n            cursor: pointer;\n            transition: all 0.3s ease;\n            display: flex;\n            align-items: center;\n            gap: 5px;\n        }\n\n        .zoom-btn:hover {\n            background-color: rgba(0, 0, 0, 0.9);\n        }\n\n        .exit-fullscreen {\n            display: none;\n        }\n\n        .pdf-container.fullscreen .exit-fullscreen {\n            display: flex;\n        }\n\n        .pdf-container.fullscreen .zoom-btn:not(.exit-fullscreen) {\n            display: none;\n        }\n\n        .fade-in {\n            animation: fadeIn 0.3s ease-in;\n        }\n\n        @keyframes fadeIn {\n            from { opacity: 0; }\n            to { opacity: 1; }\n        }\n    </style>\n</head>\n<body>\n    <div class=\"container\" id=\"container\">\n        <!-- 左侧PDF -->\n        <div class=\"pdf-container\" id=\"pdf-container-1\">\n\n            <div class=\"pdf-viewer\">\n                <embed  id = \"original_file\"\n                    src=\"\"\n                    type=\"application/pdf\"\n                    width=\"100%\"\n                    height=\"100%\"\n                />\n                <div class=\"zoom-controls\">\n                    <button class=\"zoom-btn\" onclick=\"toggleFullscreen(1)\">\n                        <i class=\"fas fa-expand\"></i> \n                    </button>\n                    <button class=\"zoom-btn exit-fullscreen\" id=\"exit-fullscreen-1\">\n                        <i class=\"fas fa-compress\"></i>\n                    </button>\n                </div>\n            </div>\n        </div>\n\n        <!-- 分隔线 -->\n        <div class=\"divider\" id=\"divider\"></div>\n\n        <!-- 右侧PDF -->\n        <div class=\"pdf-container\" id=\"pdf-container-2\">\n\n            <div class=\"pdf-viewer\">\n                <embed id =\"target_file\"\n                    src=\"../g2_zh.pdf\"\n                    type=\"application/pdf\"\n                    width=\"100%\"\n                    height=\"100%\"\n                />\n                <div class=\"zoom-controls\">\n                    <button class=\"zoom-btn\" onclick=\"toggleFullscreen(2)\">\n                        <i class=\"fas fa-expand\"></i> \n                    </button>\n                    <button class=\"zoom-btn exit-fullscreen\" id=\"exit-fullscreen-2\">\n                        <i class=\"fas fa-compress\"></i>\n                    </button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <script>\n        // 获取URL参数\nconst urlParams = new URLSearchParams(window.location.search);\nconst name = urlParams.get('name'); // 获取name参数\nconst nameTargetLanguage = urlParams.get('name_target_language'); // 获取name_target_language参数\n\n        console.log('name:', name);\n        console.log('name_target_language:', nameTargetLanguage);\n        // 获取 embed 元素\n        const embed1 = document.getElementById('original_file');\n        const embed2 = document.getElementById('target_file');\n\n        // 设置文件路径\n        embed1.src = `./static/original/${name}`; // 原文件在 original 文件夹下\n        embed2.src = `./static/target/${nameTargetLanguage}`; // 目标文件在 target 文件夹下\n\n        // 强制重新加载\n        embed1.src = embed1.src;\n         console.log('nam1e:', embed1);\n        embed2.src = embed2.src;\n\n\n\n        const divider = document.getElementById('divider');\n        const containers = document.querySelectorAll('.pdf-container');\n        let isDragging = false;\n\n        // 添加退出全屏按钮的事件监听器\n        document.getElementById('exit-fullscreen-1').addEventListener('click', () => exitFullscreen(1));\n        document.getElementById('exit-fullscreen-2').addEventListener('click', () => exitFullscreen(2));\n\n        // 拖动分隔线的功能\n        divider.addEventListener('mousedown', (e) => {\n            isDragging = true;\n            document.body.style.cursor = 'col-resize';\n            document.body.style.userSelect = 'none';\n        });\n\n        document.addEventListener('mousemove', (e) => {\n            if (!isDragging) return;\n\n            const containerRect = document.querySelector('.container').getBoundingClientRect();\n            const percentage = ((e.clientX - containerRect.left) / containerRect.width) * 100;\n\n            if (percentage > 20 && percentage < 80) {\n                containers[0].style.flex = `${percentage}`;\n                containers[1].style.flex = `${100 - percentage}`;\n            }\n        });\n\n        document.addEventListener('mouseup', () => {\n            isDragging = false;\n            document.body.style.cursor = '';\n            document.body.style.userSelect = '';\n        });\n\n        // 放大功能\n        function toggleFullscreen(pdfNum) {\n            const container = document.getElementById(`pdf-container-${pdfNum}`);\n            container.classList.add('fullscreen', 'fade-in');\n            document.getElementById('divider').style.display = 'none';\n            \n            // 隐藏另一个PDF容器\n            const otherNum = pdfNum === 1 ? 2 : 1;\n            document.getElementById(`pdf-container-${otherNum}`).style.display = 'none';\n        }\n\n        // 退出全屏功能\n        function exitFullscreen(pdfNum) {\n            const container = document.getElementById(`pdf-container-${pdfNum}`);\n            container.classList.remove('fullscreen', 'fade-in');\n            document.getElementById('divider').style.display = 'block';\n            \n            // 显示另一个PDF容器\n            const otherNum = pdfNum === 1 ? 2 : 1;\n            document.getElementById(`pdf-container-${otherNum}`).style.display = 'flex';\n            \n            // 重置flex属性\n            containers.forEach(container => {\n                container.style.flex = '1';\n            });\n        }\n\n        // ESC键退出全屏\n        document.addEventListener('keydown', (e) => {\n            if (e.key === 'Escape') {\n                containers.forEach((container, index) => {\n                    if (container.classList.contains('fullscreen')) {\n                        exitFullscreen(index + 1);\n                    }\n                });\n            }\n        });\n\n        // 双击分隔线重置布局\n        divider.addEventListener('dblclick', () => {\n            containers.forEach(container => {\n                container.style.flex = '1';\n            });\n        });\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "pdfviewer2.html",
    "content": "\n\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n  <title>Single PDF Viewer</title>\n  <style>\n    html, body {\n      margin: 0;\n      padding: 0;\n      height: 100%;\n      background-color: #f0f0f0;\n    }\n    /* 外层容器全屏 */\n    .pdf-container {\n      width: 100%;\n      height: 100%;\n    }\n    /* PDF viewer 全屏 */\n    .pdf-viewer {\n      width: 100%;\n      height: 100%;\n      /* 如果不需要边框，就直接去掉这两行 */\n      border: 1px solid #ccc;\n      border-radius: 4px;\n      background-color: #fff;\n    }\n    /* embed 元素占满父级容器 */\n    embed {\n      width: 100%;\n      height: 100%;\n      border: none;\n    }\n  </style>\n</head>\n<body>\n  <div class=\"pdf-container\">\n    <div class=\"pdf-viewer\">\n      <!-- 此处 PDF 的 src 将通过脚本根据 URL 参数设置 -->\n      <embed id=\"pdf_file\" src=\"\" type=\"application/pdf\" />\n    </div>\n  </div>\n  <script>\n    // 解析 URL 参数\n    const urlParams = new URLSearchParams(window.location.search);\n    let name = urlParams.get('name');\n    if (name && name.endsWith('.pdf')) {\n      // 去除文件名末尾的 .pdf\n      name = name.slice(0, -4);\n    }\n    const original_language = urlParams.get('original_language');\n    const target_language = urlParams.get('target_language');\n\n    // 测试用：在控制台打印文件名\n    console.log('要加载的 PDF 文件名：', name);\n\n    // 获取 embed 元素并设置 PDF 文件路径\n    const embedEl = document.getElementById('pdf_file');\n    if (name) {\n      // 根据实际文件路径修改\n      embedEl.src = `./static/merged_pdf/${name}_${original_language}_${target_language}.pdf`;\n    }\n  </script>\n</body>\n</html>\n\n"
  },
  {
    "path": "recent.json",
    "content": "[\r\n  {\r\n    \"index\": 0,\r\n    \"date\": \"2025-04-13 01:34:09\",\r\n    \"name\": \"2403.20127v1.pdf\",\r\n    \"original_language\": \"auto\",\r\n    \"target_language\": \"zh\",\r\n    \"read\": \"0\",\r\n    \"statue\": \"1\"\r\n  }\r\n]"
  },
  {
    "path": "requirements.txt",
    "content": "deepl==1.17.0\nFlask==2.0.1\nflask-cors\nPillow==10.2.0\nPyMuPDF==1.24.0\npytesseract==0.3.10\nrequests==2.31.0\ntiktoken==0.6.0\nWerkzeug==2.0.1\naiohttp\nfontTools\n\n"
  },
  {
    "path": "static/1.js",
    "content": "\n        // 在全局范围定义变量\n\n\n        // 显示主页\n        function showHome() {\n            document.getElementById('recentread').innerHTML = 'Recent Reading';\n            document.getElementById('articleContainer').style.display = '';\n            document.getElementById('viewAllSection').style.display = 'flex';\n            document.querySelector('.sidebar-menu a[onclick=\"showHome()\"]').classList.add('active');\n            document.querySelector('.sidebar-menu a[onclick=\"showAllRecent()\"]').classList.remove('active');\n            document.querySelector('.sidebar-menu a[onclick=\"showSetup()\"]').classList.remove('active'); // 添加这行\n            loadArticles(true,true);\n              document.getElementById('t-container').style.display = '';\n        }\n\n        function showAllRecent() {\n            document.getElementById('recentread').innerHTML = 'Recent Reading';\n\n            document.getElementById('articleContainer').style.display = '';\n            document.getElementById('viewAllSection').style.display = 'none';\n            document.querySelector('.sidebar-menu a[onclick=\"showHome()\"]').classList.remove('active');\n            document.querySelector('.sidebar-menu a[onclick=\"showAllRecent()\"]').classList.add('active');\n            document.querySelector('.sidebar-menu a[onclick=\"showSetup()\"]').classList.remove('active'); // 添加这行\n            loadArticles(false,true);\n            document.getElementById('t-container').style.display = '';\n        }\n        // 添加新的函数处理 Setup steps\n        function showSetup() {\n            // 隐藏其他部分（如果需要的话）\n\n\n            document.getElementById('recentread').innerHTML = 'config.json';\n            document.getElementById('articleContainer').style.display = 'none';\n             document.getElementById('viewAllSection').style.display = 'none';\n\n\n            // 移除其他菜单项的 active 类\n            document.querySelector('.sidebar-menu a[onclick=\"showHome()\"]').classList.remove('active');\n            document.querySelector('.sidebar-menu a[onclick=\"showAllRecent()\"]').classList.remove('active');\n\n            // 给 Setup steps 添加 active 类\n            document.querySelector('.sidebar-menu a[onclick=\"showSetup()\"]').classList.add('active');\n            document.getElementById('t-container').style.display = 'block';\n        }\n\n        // 显示上传模态框\n        function showUpload() {\n            document.getElementById('uploadModal').style.display = 'block';\n            document.getElementById('upload_content-1').style.display = 'block';\n            document.getElementById('upload_content-2').style.display = 'none';\n            document.getElementById('languageSelection').style.display = 'none';\n\n        }\n\n\n\n        // 显示设置模态框\n        function showSettings() {\n            document.getElementById('settingsModal').style.display = 'block';\n        }\n\n\nasync function loadArticles(isLimited,first_reload) {\n    const container = document.getElementById('articleContainer');\n    if (first_reload) {\n                const record_show_staute = document.getElementById('record_show_staute');\n        record_show_staute.setAttribute('data-value', isLimited);\n    }\n\n\n\n\ntry {\n    container.innerHTML = '<div class=\"loading\">正在加载数据...</div>';\n\n    const response = await fetch('/recent.json');\n    if (!response.ok) {\n        throw new Error(`HTTP error! status: ${response.status}`);\n    }\n\n    const data = await response.json();\n    container.innerHTML = '';\n\n    if (data.length === 0) {\n        container.innerHTML = '<div class=\"loading\">No reading records yet</div>';\n        return;\n    }\n\n    // 根据 index 排序(从大到小)\n    let sortedArticles = [...data].sort((a, b) => b.index - a.index);\n\n    // 如果需要限制显示数量\n    if (isLimited) {\n        sortedArticles = sortedArticles.slice(0, 3);\n    }\n\n    sortedArticles.forEach(article => {\n        const articleCard = document.createElement('a');\n        articleCard.className = 'article-card';\n\n        // 上半部分div\n        const topDiv = document.createElement('div');\n        topDiv.className = 'article-top';\n        topDiv.innerHTML = `\n            <img src=\"./static/thumbnail/${article.name.substring(0, article.name.lastIndexOf('.'))}.png\" alt=\"${article.name}\">\n\n        `;\n\n\n        // 下半部分div\n        const bottomDiv = document.createElement('div');\n        bottomDiv.className = 'article-bottom';\n\n        // 文章标题\n        const titleDiv = document.createElement('div');\n        titleDiv.className = 'article-title';\n        titleDiv.innerHTML = `<h3>${article.name}</h3>`;\n\n        // 信息行div\n        const infoDiv = document.createElement('div');\n        infoDiv.className = 'article-info';\n        infoDiv.innerHTML = `\n            <span class=\"author\">${article.author || 'Unknown author'}</span>\n            <span class=\"date\">${article.date}</span>\n            <span class=\"language\">${article.original_language} - ${article.target_language}</span>\n        `;\n\n        bottomDiv.appendChild(titleDiv);\n        bottomDiv.appendChild(infoDiv);\n\n        // 状态指示器\n        const statusIndicator = document.createElement('div');\n        statusIndicator.className = 'status-indicator';\n\n        if (parseInt(article.statue) === 0) {\n            statusIndicator.innerHTML = '<i class=\"fas fa-spinner fa-spin\"></i>';\n            articleCard.className += ' disabled';\n            articleCard.addEventListener('click', (e) => {\n                e.preventDefault();\n                showToast('Translation is not complete yet, unable to view at this time.');\n            });\n        } else {\n            statusIndicator.innerHTML = '<i class=\"fas fa-check\"></i>';\n            articleCard.addEventListener('click', () => {\n                const targetFileName = `${article.name.replace(/\\.pdf$/, '')}_${article.target_language}.pdf`;\n                const url = `/pdfviewer.html?name=${encodeURIComponent(article.name)}&name_target_language=${encodeURIComponent(targetFileName)}&index=${encodeURIComponent(article.index)}`;\n                window.open(url, '_blank');\n            });\n            articleCard.style.cursor = 'pointer';\n\n        }\n        bottomDiv.appendChild(statusIndicator);\n\n        // 阅读状态标签\n        const readStatus = document.createElement('div');\n        readStatus.className = `read-status ${parseInt(article.read) === 0 ? 'unread' : 'read'}`;\n        readStatus.textContent = parseInt(article.read) === 0 ? 'Unread' : 'Read';\n\n        // 三点菜单按钮\n        const menuButton = document.createElement('button');\n        menuButton.className = 'menu-button';\n        menuButton.innerHTML = '<i class=\"fas fa-ellipsis-v\"></i>';\n\n        articleCard.appendChild(topDiv);\n        articleCard.appendChild(bottomDiv);\n        articleCard.appendChild(readStatus);\n        articleCard.appendChild(menuButton);\n\n        container.appendChild(articleCard);\n\n        // 菜单按钮点击事件\n        menuButton.addEventListener('click', (e) => {\n            e.preventDefault();\n            e.stopPropagation();\n            showMenu(e, article, e.currentTarget);\n        });\n    });\n} catch (error) {\n    console.error('加载数据失败:', error);\n    container.innerHTML = `\n        <div class=\"error\">\n            加载数据失败，请稍后重试<br>\n            <small>${error.message}</small>\n        </div>\n    `;\n}\n\n}\n\n// 显示菜单函数\n\n\n\n\n// Toast提示函数\nfunction showToast(message) {\n    const toast = document.createElement('div');\n    toast.className = 'toast';\n    toast.textContent = message;\n    document.body.appendChild(toast);\n\n    setTimeout(() => {\n        toast.remove();\n    }, 2000);\n}\n\n\n// 显示菜单函数\nfunction showMenu(event, article) {\n    const menu = document.createElement('div');\n    articleId = article.index\n    article_name= article.name\n    article_tl = article.target_language\n    article_ol = article.original_language\n    console.log(2,articleId)\n    menu.className = 'article-menu';\n    menu.innerHTML = `\n        <div class=\"menu-item\" onclick=\"editArticle(${articleId})\">Edit</div>\n        <div class=\"menu-item\" onclick=\"deleteArticle(${articleId})\">Delete</div>\n       <div class=\"menu-item\" onclick=\"open_bilingual(${articleId}, '${article_name}', '${article_tl}', '${article_ol}')\">Bilingual PDF</div>\n    `;\n\n    // 定位菜单\n    menu.style.position = 'absolute';\n     menu.style.top = `${event.pageY}px`;\n     menu.style.left = `${event.pageX}px`;\n\n    document.body.appendChild(menu);\n\n    // 点击其他地方关闭菜单\n    document.addEventListener('click', function closeMenu(e) {\n        if (!menu.contains(e.target) && e.target !== event.target) {\n            menu.remove();\n            document.removeEventListener('click', closeMenu);\n        }\n    });\n}\n\n\nfunction open_bilingual(articleId,article_name,article_tl,article_ol) {\n    const url = `/pdfviewer2.html?name=${encodeURIComponent(article_name)}&target_language=${encodeURIComponent(article_tl)}&index=${encodeURIComponent(articleId)}&original_language=${encodeURIComponent(article_ol)}`;\n    window.open(url, '_blank');\n}\n\n\n// Toast提示函数\nfunction showToast(message) {\n    const toast = document.createElement('div');\n    toast.className = 'toast';\n    toast.textContent = message;\n    document.body.appendChild(toast);\n\n    setTimeout(() => {\n        toast.remove();\n    }, 2000);\n}\n\n\n\n        // 页面加载完成后初始化\n        document.addEventListener('DOMContentLoaded', function() {\n            showHome();\n        });\nfunction closeUploadModal() {\n    // 隐藏modal\n    document.getElementById('uploadModal').style.display = 'none';\n    // 清空文件列表显示\n    document.getElementById('uploadFilesList').innerHTML = '';\n    // 清空uploadFiles Map\n    uploadFiles.clear();\n\n    // 重置上传界面（如果需要的话）\n    document.getElementById('upload_content-1').style.display = 'flex';\n    document.getElementById('upload_content-2').style.display = 'none';\n}\n\n\n"
  },
  {
    "path": "static/2.js",
    "content": "// 全局变量存储API密钥\nlet translationKeys = {\n    AI302: '', \n    deepl: '',\n    google: '',\n    youdao: '',\n    aliyun: '',\n    tencent: '',\n    Grok: '',  // 修改为大写的Grok\n    ThirdParty: '',  // 添加ThirdParty\n    GLM: '',  // 添加GLM\n    bing: ''  // 添加Bing\n};\n\n// 关闭设置弹窗\nfunction closeSettings() {\n    document.getElementById('settingsModal').style.display = 'none';\n\n}\nconst toggle = document.getElementById('ocrToggle');\nconst toggle2 = document.getElementById('translationToggle');\nfunction getValue() {\n    return toggle.checked ?\n           toggle.getAttribute('data-on') :\n           toggle.getAttribute('data-off');\n}\nfunction getValue2() {\n    return toggle2.checked ?\n           toggle2.getAttribute('data-on') :\n           toggle.getAttribute('data-off');\n}\n\n\nfunction getecount() {\n    fetch('/api/get-default-services')\n        .then(response => response.json())\n        .then(data => {\n            if (data.success && data.data) {\n                const settings = data.data;\n\n                document.getElementById('count_article').textContent = ` Articles in Total: ${settings.count} `;\n            }\n        })\n        .catch(error => {\n            console.error('获取设置失败:', error);\n            alert('获取设置失败，请稍后重试');\n        });\n}\n\n\n\n"
  },
  {
    "path": "static/3.js",
    "content": "let uploadFiles = new Map();\r\n// 文件输入处理\r\nconst fileInput = document.getElementById('fileInput');\r\n\r\nfunction triggerFileInput() {\r\n    fileInput.click();\r\n}\r\n\r\n// 文件输入事件监听\r\nfileInput.addEventListener('change', (e) => {\r\n    const files = e.target.files;\r\n    if (files && files.length > 0) {\r\n        handleFiles({ files: files });\r\n    }\r\n    // 清空文件输入框，允许重复选择同一文件\r\n    fileInput.value = '';\r\n});\r\n\r\n// 拖拽区域处理\r\nconst dropZone = document.getElementById('dropZone');\r\ndropZone.addEventListener('dragover', (e) => {\r\n    e.preventDefault();\r\n    dropZone.classList.add('dragover');\r\n});\r\n\r\ndropZone.addEventListener('dragleave', (e) => {\r\n    e.preventDefault();\r\n    dropZone.classList.remove('dragover');\r\n});\r\n\r\ndropZone.addEventListener('drop', (e) => {\r\n    e.preventDefault();\r\n    dropZone.classList.remove('dragover');\r\n    handleFiles(e.dataTransfer);\r\n});\r\n\r\nasync function handleFiles(input) {\r\n    const files = input.files;\r\n    const filesList = document.getElementById('uploadFilesList');\r\n\r\n    if (!files || files.length === 0) {\r\n        return;\r\n    }\r\n\r\n    // 检查总文件数量\r\n    if (uploadFiles.size + files.length > 12) {\r\n        showError('最多只能上传12个文件');\r\n        return;\r\n    }\r\n\r\n    // 使用Promise.all并行处理所有文件\r\n    const uploadPromises = Array.from(files).map(async (file) => {\r\n        // 处理拖拽的文件对象\r\n        if (file.kind === 'file') file = file.getAsFile();\r\n\r\n        // 检查文件是否重复\r\n        const isDuplicate = Array.from(uploadFiles.values()).some(existingFile =>\r\n            existingFile.file.name === file.name &&\r\n            existingFile.file.size === file.size\r\n        );\r\n\r\n        if (isDuplicate) {\r\n            showError(`文件已存在: ${file.name}`);\r\n            return;\r\n        }\r\n\r\n        // 检查文件类型\r\n        const validTypes = ['.pdf', '.xps', '.epub', '.fb2', '.cbz', '.mobi'];\r\n        const fileExt = '.' + file.name.split('.').pop().toLowerCase();\r\n        if (!validTypes.includes(fileExt)) {\r\n            showError(`不支持的文件格式: ${file.name}`);\r\n            return;\r\n        }\r\n\r\n        // 检查文件大小\r\n        if (file.size > 200 * 1024 * 1024) {\r\n            showError(`文件过大: ${file.name}`);\r\n            return;\r\n        }\r\n\r\n        // 创建文件ID\r\n        const fileId = Date.now() + Math.random().toString(36).substr(2, 9);\r\n\r\n        // 添加到上传列表\r\n        uploadFiles.set(fileId, {\r\n            file: file,\r\n            status: 'pending'\r\n        });\r\n\r\n        // 创建并添加文件项UI\r\n        const fileItem = createFileItemUI(fileId, file);\r\n        filesList.appendChild(fileItem);\r\n\r\n        // 开始上传\r\n        return uploadFile(fileId);\r\n    });\r\n\r\n    // 等待所有文件上传完成\r\n    await Promise.all(uploadPromises);\r\n\r\n    // 检查是否有文件上传成功\r\n    if ([...uploadFiles.values()].some(f => f.status === 'success')) {\r\n        document.getElementById('languageSelection').style.display = 'flex';\r\n    }\r\n}\r\n\r\n// 创建文件项UI\r\nfunction createFileItemUI(fileId, file) {\r\n    const fileItem = document.createElement('div');\r\n    fileItem.className = 'file-item';\r\n    fileItem.id = `file-${fileId}`;\r\n\r\n    const fileName = document.createElement('span');\r\n    fileName.className = 'file-name';\r\n    fileName.textContent = file.name;\r\n\r\n    const fileStatus = document.createElement('span');\r\n    fileStatus.className = 'file-status';\r\n    fileStatus.textContent = '准备上传';\r\n\r\n\r\n\r\n    const progressBar = document.createElement('div');\r\n    progressBar.className = 'progress-bar';\r\n    progressBar.style.width = '0%';\r\n\r\n    fileItem.appendChild(fileName);\r\n    fileItem.appendChild(fileStatus);\r\n\r\n    fileItem.appendChild(progressBar);\r\n\r\n    return fileItem;\r\n}\r\n\r\n// 移除文件\r\nfunction removeFile(fileId) {\r\n    const fileItem = document.getElementById(`file-${fileId}`);\r\n    if (fileItem) {\r\n        fileItem.remove();\r\n        uploadFiles.delete(fileId);\r\n    }\r\n\r\n    // 如果没有成功上传的文件，隐藏语言选择\r\n    if (![...uploadFiles.values()].some(f => f.status === 'success')) {\r\n        document.getElementById('languageSelection').style.display = 'none';\r\n    }\r\n}\r\n\r\n// 上传文件\r\n// async function uploadFile(fileId) {\r\n//     const fileData = uploadFiles.get(fileId);\r\n//     if (!fileData) return;\r\n//\r\n//     const fileItem = document.getElementById(`file-${fileId}`);\r\n//     const statusElement = fileItem.querySelector('.file-status');\r\n//     const progressBar = fileItem.querySelector('.progress-bar');\r\n//\r\n//     try {\r\n//         // 创建 FormData\r\n//         const formData = new FormData();\r\n//         formData.append('file', fileData.file);\r\n//         console.log(formData,'file name')\r\n//\r\n//         // 发送上传请求\r\n//         const response = await fetch('/upload', {\r\n//             method: 'POST',\r\n//             body: formData,\r\n//             onUploadProgress: (progressEvent) => {\r\n//                 const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);\r\n//                 progressBar.style.width = percentCompleted + '%';\r\n//                 statusElement.textContent = 'Uploading ' + percentCompleted + '%';\r\n//             }\r\n//         });\r\n//\r\n//         if (response.ok) {\r\n//             statusElement.textContent = 'Upload Success';\r\n//             fileData.status = 'success';\r\n//             console.log(11,fileData.file)\r\n//             progressBar.style.backgroundColor = '#4CAF50';\r\n//         } else {\r\n//             throw new Error('Upload failed');\r\n//         }\r\n//     } catch (error) {\r\n//         statusElement.textContent = 'Upload failure';\r\n//         fileData.status = 'error';\r\n//         progressBar.style.backgroundColor = '#f44336';\r\n//         showError(`Upload failure: ${fileData.file.name}`);\r\n//     }\r\n// }\r\n\r\n// 显示错误信息\r\nfunction showError(message) {\r\n    // 实现错误提示的显示逻辑\r\n    console.error(message);\r\n    // 可以添加一个toast或者其他UI提示\r\n}\r\n\r\nfunction createFileItemUI(fileId, file) {\r\n    const div = document.createElement('div');\r\n    div.className = 'upload-file-item';\r\n    div.id = `file-${fileId}`;\r\n\r\n    div.innerHTML = `\r\n\r\n        <div class=\"upload-file-info\">\r\n            <div class=\"upload-file-name\">${file.name}</div>\r\n            <div class=\"upload-file-meta\">\r\n                ${formatFileSize(file.size)}\r\n            </div>\r\n        </div>\r\n        <div class=\"upload-status status-pending\">等待上传</div>\r\n    `;\r\n\r\n    return div;\r\n}\r\n\r\nasync function uploadFile(fileId) {\r\n    const fileData = uploadFiles.get(fileId);\r\n    const statusEl = document.querySelector(`#file-${fileId} .upload-status`);\r\n\r\n    try {\r\n        statusEl.className = 'upload-status status-uploading';\r\n        statusEl.textContent = 'uploading';\r\n\r\n        const formData = new FormData();\r\n        formData.append('file', fileData.file);\r\n\r\n\r\n        const response = await fetch('/upload/', {\r\n            method: 'POST',\r\n            body: formData\r\n        });\r\n\r\n        if (!response.ok) throw new Error('Upload failed');\r\n\r\n        const result = await response.json();\r\n\r\n        statusEl.className = 'upload-status status-success';\r\n        statusEl.textContent = 'Upload Success';\r\n        fileData.status = 'success';\r\n\r\n    } catch (error) {\r\n        console.error('Upload error:', error);\r\n        statusEl.className = 'upload-status status-error';\r\n        statusEl.textContent = '上传失败';\r\n        fileData.status = 'error';\r\n    }\r\n}\r\n\r\nfunction formatFileSize(bytes) {\r\n    if (bytes === 0) return '0 B';\r\n    const k = 1024;\r\n    const sizes = ['B', 'KB', 'MB', 'GB'];\r\n    const i = Math.floor(Math.log(bytes) / Math.log(k));\r\n    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\r\n}\r\n\r\nfunction showError(message) {\r\n    alert(message);\r\n}\r\n\r\nasync function handleNextStep() {\r\n    // 获取所有成功上传的文件名\r\n    const successFiles = [...uploadFiles.entries()]\r\n        .filter(([_, data]) => data.status === 'success')\r\n        .map(([_, data]) => data.file.name);\r\n\r\n    // 获取选择的语言\r\n    const sourceLang = document.getElementById('sourceLang').value;\r\n    const targetLang = document.getElementById('targetLang').value;\r\n\r\n    try {\r\n            const response = await fetch('/config_json')\r\n            if (!response.ok) {\r\n                throw new Error('Network response was not ok');\r\n            }\r\n\r\n            // 使用 response.json() 來解析 JSON 數據\r\n            const { translation_services, default_services } = await response.json();\r\n            \r\n            if(translation_services && default_services){\r\n                // 获取当前选择的翻译服务\r\n                const currentTranslationService = default_services.Translation_api;\r\n                \r\n                // 如果当前选择的不是Bing翻译\r\n                if(currentTranslationService !== 'bing'){\r\n                    // 检查当前选择的翻译服务是否配置了API密钥\r\n                    const currentService = translation_services[currentTranslationService];\r\n                    if(!(currentService && (currentService['auth_key'] || currentService['app_key']))){\r\n                        throw new Error('not config translation services authKey');\r\n                    }\r\n                }\r\n            }else{\r\n                throw new Error('data error');\r\n            }\r\n            \r\n            // 1秒后切换界面的Promise\r\n            const switchUIPromise = new Promise((resolve) => {\r\n                setTimeout(() => {\r\n                    document.getElementById('upload_content-1').style.display = 'none';\r\n                    document.getElementById('upload_content-2').style.display = 'flex';\r\n                    resolve();\r\n                }, 1000);\r\n            });\r\n\r\n\r\n            // 异步发起翻译请求\r\n            fetch('/translation', {\r\n                method: 'POST',\r\n                headers: {\r\n                    'Content-Type': 'application/json'\r\n                },\r\n                body: JSON.stringify({\r\n                    files: successFiles,\r\n                    sourceLang: sourceLang,\r\n                    targetLang: targetLang\r\n                })\r\n            })\r\n            .then(response => response.json())\r\n            .then(data => {\r\n                if (data.status === 'success') {\r\n                    // 执行回调函数\r\n                    const value = document.getElementById('record_show_staute').getAttribute('data-value') === 'true';\r\n                    loadArticles(value,false);\r\n                     getecount();\r\n\r\n\r\n                } else {\r\n                    throw new Error('Translation request failed');\r\n                }\r\n            })\r\n            .catch(error => {\r\n                console.error('Translation request error:', error);\r\n                // showError('翻译请求失败，请稍后重试');\r\n            });\r\n\r\n            // 等待1秒后的界面切换\r\n            await switchUIPromise;\r\n\r\n        } catch (error) {\r\n        console.error('Error:', error);\r\n        alert(error)\r\n        // showError('操作失败，请稍后重试');\r\n    }\r\n\r\n\r\n}\r\n\r\n function updatecount() {\r\n    fetch('/config_json')\r\n        .then(response => response.json())\r\n        .then(data => {\r\n           document.getElementById('count_article').textContent += data.count;\r\n        });\r\n    }\r\n\r\nfunction deleteArticle(articleId) {\r\n    // 创建弹窗遮罩层\r\n    const overlay = document.createElement('div');\r\n    console.log(articleId)\r\n    overlay.style.cssText = `\r\n        position: fixed;\r\n        top: 0;\r\n        left: 0;\r\n        width: 100%;\r\n        height: 100%;\r\n        background: rgba(0, 0, 0, 0.5);\r\n        z-index: 1000;\r\n        display: flex;\r\n        justify-content: center;\r\n        align-items: center;\r\n    `;\r\n\r\n    // 创建弹窗内容\r\n    const modal = document.createElement('div');\r\n    modal.style.cssText = `\r\n        background: white;\r\n        border-radius: 8px;\r\n        padding: 24px;\r\n        width: 400px;\r\n        text-align: center;\r\n    `;\r\n\r\n    // 创建弹窗标题\r\n    const title = document.createElement('h3');\r\n    title.textContent = 'Delete article';\r\n    title.style.cssText = `\r\n        margin: 0;\r\n        margin-bottom: 8px;\r\n        font-size: 18px;\r\n        color: #333;\r\n    `;\r\n\r\n    // 创建弹窗提示文本\r\n    const message = document.createElement('p');\r\n    message.textContent = 'Are you sure you want to delete the selected items? This action cannot be undone.';\r\n    message.style.cssText = `\r\n        margin: 16px 0;\r\n        color: #666;\r\n        font-size: 14px;\r\n    `;\r\n\r\n    // 创建按钮容器\r\n    const buttonContainer = document.createElement('div');\r\n    buttonContainer.style.cssText = `\r\n        display: flex;\r\n        justify-content: center;\r\n        gap: 12px;\r\n        margin-top: 24px;\r\n    `;\r\n\r\n    // 创建取消按钮\r\n    const cancelButton = document.createElement('button');\r\n    cancelButton.textContent = 'Cancel';\r\n    cancelButton.style.cssText = `\r\n        padding: 8px 24px;\r\n        border: none;\r\n        border-radius: 4px;\r\n        background: #f5f5f5;\r\n        color: #333;\r\n        cursor: pointer;\r\n    `;\r\n\r\n    // 创建删除按钮\r\n    const confirmButton = document.createElement('button');\r\n    confirmButton.textContent = 'Delete';\r\n    confirmButton.style.cssText = `\r\n        padding: 8px 24px;\r\n        border: none;\r\n        border-radius: 4px;\r\n        background: #ff4d4f;\r\n        color: white;\r\n        cursor: pointer;\r\n    `;\r\n\r\n    // 添加按钮点击事件\r\n    cancelButton.onclick = () => {\r\n        document.body.removeChild(overlay);\r\n    };\r\n\r\n    confirmButton.onclick = async () => {\r\n        try {\r\n            const response = await fetch('/delete_article', {\r\n                method: 'POST',\r\n                headers: {\r\n                    'Content-Type': 'application/json'\r\n                },\r\n                body: JSON.stringify({ articleId })\r\n            });\r\n\r\n            if (response.ok) {\r\n                // 删除成功，移除弹窗并刷新页面\r\n                document.body.removeChild(overlay);\r\n\r\n                const value = document.getElementById('record_show_staute').getAttribute('data-value') === 'true';\r\n                loadArticles(value,false)\r\n                getecount()\r\n            } else {\r\n                throw new Error('删除失败');\r\n            }\r\n        } catch (error) {\r\n            console.error('删除文章失败:', error);\r\n            alert('删除失败，请重试');\r\n        }\r\n    };\r\n\r\n    // 组装弹窗\r\n    buttonContainer.appendChild(cancelButton);\r\n    buttonContainer.appendChild(confirmButton);\r\n    modal.appendChild(title);\r\n    modal.appendChild(message);\r\n    modal.appendChild(buttonContainer);\r\n    overlay.appendChild(modal);\r\n\r\n    // 将弹窗添加到页面\r\n    document.body.appendChild(overlay);\r\n}\r\n\r\n\r\n"
  },
  {
    "path": "static/4.js",
    "content": "\n// 用于存储批量选择的文章ID\nlet selectedBatchIds = new Set();\n\n// 展示批量管理弹窗\nfunction showBatchModal() {\n  document.getElementById('batchModal').style.display = 'block';\n  loadBatchData(); // 获取数据并渲染卡片\n}\n\n// 关闭批量管理弹窗\nfunction closeBatchModal() {\n  document.getElementById('batchModal').style.display = 'none';\n  // 关闭时清空已选\n  selectedBatchIds.clear();\n}\n\n// 加载 recent.json 数据并渲染到批量弹窗\nasync function loadBatchData() {\n  const container = document.getElementById('batchGrid');\n  container.innerHTML = '<div class=\"loading\">Loading data...</div>';\n  try {\n    const response = await fetch('/recent.json');\n    if (!response.ok) {\n      throw new Error(`HTTP error! status: ${response.status}`);\n    }\n    const data = await response.json();\n    container.innerHTML = '';\n\n    if (!data || data.length === 0) {\n      container.innerHTML = '<div class=\"loading\">No records to batch manage</div>';\n      return;\n    }\n\n    // 按 index 倒序\n    const sortedData = data.sort((a, b) => b.index - a.index);\n\n    // 将卡片渲染到 container\n    sortedData.forEach(item => {\n      const card = document.createElement('div');\n      card.className = 'batch-card';\n      card.dataset.indexId = item.index; // 存一下，方便后续操作\n\n      // 已读 / 未读\n      const readStatus = item.read === \"1\" ? \"Read\" : \"Unread\";\n\n      // 注意：后端返回没有作者的话，可以用Unknown\n      const author = item.author || \"Unknown author\";\n      const original_lan = item.original_language ;\n      const target_lan = item.target_language;\n\n      card.innerHTML = `\n        <div class=\"batch-card-title\"><strong>${item.name}</strong></div>\n        <div class=\"batch-card-info\">\n          <p>Date: ${item.date}</p>\n          <p>Author: ${author}</p>\n          <p>Status: ${readStatus}  ||  Convertion: \n\n${original_lan} to ${target_lan}</p>\n     \n        </div>\n      `;\n\n      // 点击选择或取消选择\n      card.addEventListener('click', () => {\n        if (selectedBatchIds.has(item.index)) {\n          selectedBatchIds.delete(item.index);\n          card.classList.remove('selected');\n        } else {\n          selectedBatchIds.add(item.index);\n          card.classList.add('selected');\n        }\n      });\n\n      container.appendChild(card);\n    });\n  } catch (error) {\n    console.error('加载数据失败:', error);\n    container.innerHTML = `<div class=\"error\">Failed to load data<br>${error.message}</div>`;\n  }\n}\n\n// 全选 / 取消全选\nfunction toggleSelectAll() {\n  const container = document.getElementById('batchGrid');\n  const cards = container.querySelectorAll('.batch-card');\n\n  // 如果有一个未选，则本次点击后全选，否则取消全选\n  let shouldSelectAll = false;\n  if (selectedBatchIds.size < cards.length) {\n    // 还有没选的，进行全选\n    shouldSelectAll = true;\n  }\n\n  cards.forEach(card => {\n    const indexId = parseInt(card.dataset.indexId, 10);\n    if (shouldSelectAll) {\n      selectedBatchIds.add(indexId);\n      card.classList.add('selected');\n    } else {\n      selectedBatchIds.delete(indexId);\n      card.classList.remove('selected');\n    }\n  });\n}\n\n// 批量删除\nasync function handleBatchDelete() {\n  if (selectedBatchIds.size === 0) {\n    alert('No articles selected!');\n    return;\n  }\n\n  // 简单确认\n  if (!confirm('Are you sure you want to delete the selected items?')) {\n    return;\n  }\n\n  // 发送到后端\n  try {\n    // 假设后端你新加了一个 /delete_batch 接口\n    const response = await fetch('/delete_batch', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json'\n      },\n      body: JSON.stringify({\n        articleIds: Array.from(selectedBatchIds)\n      })\n    });\n\n    if (!response.ok) throw new Error('Delete failed');\n\n    // 删除成功后刷新弹窗数据\n    selectedBatchIds.clear();\n    loadBatchData();\n    getecount();\n  } catch (error) {\n    console.error('删除失败:', error);\n    alert('Delete failed, please try again!');\n  }\n}\n\n// 生成思维导图\nfunction handleMindMap() {\n  if (selectedBatchIds.size === 0) {\n    alert('No articles selected for mind map!');\n    return;\n  }\n\n  // 这里演示直接在控制台输出，你可以改为实际的请求\n  console.log('生成思维导图，选中的ID:', Array.from(selectedBatchIds));\n  alert('Pretend to generate Mind Map for selected items');\n}\n\n// 总结\nfunction handleSummary() {\n  if (selectedBatchIds.size === 0) {\n    alert('No articles selected for summary!');\n    return;\n  }\n\n  // 同上，这里可以改成实际的后端接口\n  console.log('生成总结，选中的ID:', Array.from(selectedBatchIds));\n  alert('Pretend to generate Summary for selected items');\n}\n"
  },
  {
    "path": "static/i18n.js",
    "content": "const i18n = {\r\nen: {\r\n    // 页面中用到的 key: value\r\n    '1': 'Library',\r\n    '2': 'Hosmepage',\r\n    '3': 'Recent Reading',\r\n    '4': 'Setup steps',\r\n    '5': 'Search',\r\n    '6': 'Folders',\r\n    '7': 'Recent Reading',\r\n    '8': 'Batch',\r\n    '9': 'Settings',\r\n    '10': '+ Add Article',\r\n    '11': '中文',\r\n    '12': 'Articles in Total:',\r\n    '13': 'View All >',\r\n    '14': 'Config File Editor',\r\n    '15': 'Save All Changes',\r\n    '16': 'Chinese LLM API Application',\r\n    '17': 'Apply via Volcengine Platform:',\r\n    '18': 'Application Link: <a href=\"https://www.volcengine.com/product/doubao/\" style=\"color: #0066cc; text-decoration: none;\">Volcano-Doubao</a>',\r\n    '19': 'Supported Models: Doubao, Deepseek series models',\r\n    '20': 'Apply via Alibaba Cloud Platform:',\r\n    '21': 'Application Link: <a href=\"https://cn.aliyun.com/product/tongyi?from_alibabacloud=&utm_content=se_1019997984\" style=\"color: #0066cc; text-decoration: none;\">Alicloud-Qwen</a>',\r\n    '22': 'Supported Models: Qwen-Max, Qwen-Plus, and other series models',\r\n    '23': 'count:',\r\n    '24': 'PPC:',\r\n    '25': 'Translation Model API',\r\n    '26': 'OCR Services',\r\n    '27': 'Default Configuration',\r\n    '28': 'Add Literature to Your Reading List',\r\n    '29': 'Drag and drop your files here or <span class=\"upload-link\" onclick=\"triggerFileInput()\"> click to upload</span>',\r\n    '30': 'Up to 12 files at a time, each with a maximum of 200MB',\r\n    '31': 'Supported : PDF, XPS, EPUB, FB2, CBZ, MOBI',\r\n    '32': 'Please wait patiently, you can check the translation progress under the recent reading tab.',\r\n    '33': 'Translation API Settings',\r\n    '34': 'Openai API',\r\n    '35': 'Doubao Translate API',\r\n    '36': 'Qwen Translate API',\r\n    '37': 'Deepseek API',\r\n    '38': 'deepl API',\r\n    '39': 'Youdao Translation API',\r\n    '40': 'OCR Global Mode Disabled by Default',\r\n    '41': 'Translation Mode Enabled',\r\n    '42': 'Batch Management',\r\n    '43': 'Select All',\r\n    '44': 'Delete',\r\n    '45': 'Mind Map',\r\n    '46': 'Summary',\r\n    '47':'    Notes:\\n' +\r\n        '\\n' +\r\n        '    <ul>\\n' +\r\n        '        <li>OCR and Line modes are disabled by default\\n' +\r\n        '            <ul> PPC (Pages Per Call)\\n' +\r\n        '                <li>OCR is used for scanning image-based PDFs</li>\\n' +\r\n        '                <li>Line mode is used for translating sparse PDFs</li>\\n' +\r\n        '            </ul>\\n' +\r\n        '        </li>\\n' +\r\n        '        <li>The translation feature is enabled by default</li>\\n' +\r\n        '        <li>After changing the translation model, please enter the corresponding model name and key in the translation model API options</li>\\n' +\r\n        '    </ul>',\r\n    '48': 'Grok Translate API',\r\n    '49': 'Third Party API',\r\n    '50': 'GLM Translate API',\r\n    '51': 'Bing Translate API',\r\n    '52': 'Translation Prompt',\r\n    '53': '302.ai API'\r\n\r\n},\r\n\r\n  \"zh\": {\r\n    \"1\": \"资料库\",\r\n    \"2\": \"首页\",\r\n    \"3\": \"最近阅读\",\r\n    \"4\": \"设置步骤\",\r\n    \"5\": \"搜索\",\r\n    \"6\": \"文件夹\",\r\n    \"7\": \"最近阅读\",\r\n    \"8\": \"批量\",\r\n    \"9\": \"设置\",\r\n    \"10\": \"+ 添加文章\",\r\n    \"11\": \"中文\",\r\n    \"12\": \"文章总计：\",\r\n    \"13\": \"查看全部 >\",\r\n    \"14\": \"配置文件编辑器\",\r\n    \"15\": \"保存所有更改\",\r\n    \"16\": \"中文 LLM API 申请\",\r\n    \"17\": \"通过火山引擎平台申请：\",\r\n    \"18\": \"申请链接: <a href=\\\"https://www.volcengine.com/product/doubao/\\\" style=\\\"color: #0066cc; text-decoration: none;\\\">Volcano-Doubao</a>\",\r\n    \"19\": \"支持的模型: Doubao, Deepseek 系列模型\",\r\n    \"20\": \"通过阿里云平台申请：\",\r\n    \"21\": \"申请链接: <a href=\\\"https://cn.aliyun.com/product/tongyi?from_alibabacloud=&utm_content=se_1019997984\\\" style=\\\"color: #0066cc; text-decoration: none;\\\">Alicloud-Qwen</a>\",\r\n    \"22\": \"支持的模型: Qwen-Max, Qwen-Plus, 以及其他系列模型\",\r\n    \"23\": \"计数：\",\r\n    \"24\": \"PPC：\",\r\n    \"25\": \"翻译模型 API\",\r\n    \"26\": \"OCR 服务\",\r\n    \"27\": \"默认配置\",\r\n    \"28\": \"将文献添加到您的阅读列表\",\r\n    \"29\": \"将文件拖拽到这里，或<span class=\\\"upload-link\\\" onclick=\\\"triggerFileInput()\\\">点击上传</span>\",\r\n    \"30\": \"一次最多可以上传 12 个文件，每个文件最大 200MB\",\r\n    \"31\": \"支持：PDF, XPS, EPUB, FB2, CBZ, MOBI\",\r\n    \"32\": \"请耐心等待，您可以在最近阅读标签下查看翻译进度。\",\r\n    \"33\": \"翻译 API 设置\",\r\n    \"34\": \"Openai API\",\r\n    \"35\": \"Doubao 翻译 API\",\r\n    \"36\": \"Qwen 翻译 API\",\r\n    \"37\": \"Deepseek API\",\r\n    \"38\": \"deepl API\",\r\n    \"39\": \"有道翻译 API\",\r\n    \"40\": \"默认禁用 OCR 全局模式\",\r\n    \"41\": \"已启用翻译模式\",\r\n    \"42\": \"批量管理\",\r\n    \"43\": \"全选\",\r\n    \"44\": \"删除\",\r\n    \"45\": \"思维导图\",\r\n    \"46\": \"摘要\",\r\n    \"47\": \"注意:\\n\\n<ul>\\n<li>OCR 和 Line 模式默认禁用\\n  <ul> PPC（每次调用的页面数）\\n    <li>OCR 用于扫描基于图像的 PDF</li>\\n   \" +\r\n        \"\\n</ul>\",\r\n    \"48\": \"Grok 翻译 API\",\r\n    \"49\": \"第三方 API\",\r\n    \"50\": \"智谱 GLM 翻译 API\",\r\n    \"51\": \"必应翻译 API\",\r\n    \"52\": \"翻译提示词\",\r\n    \"53\": \"302.ai API\"\r\n\r\n  },\r\n  \"es\": {\r\n    \"1\": \"Biblioteca\",\r\n    \"2\": \"Página principal\",\r\n    \"3\": \"Lectura reciente\",\r\n    \"4\": \"Pasos de configuración\",\r\n    \"5\": \"Buscar\",\r\n    \"6\": \"Carpetas\",\r\n    \"7\": \"Lectura reciente\",\r\n    \"8\": \"Procesar en lote\",\r\n    \"9\": \"Ajustes\",\r\n    \"10\": \"+ Agregar artículo\",\r\n    \"11\": \"Chino\",\r\n    \"12\": \"Artículos en total:\",\r\n    \"13\": \"Ver todo >\",\r\n    \"14\": \"Editor de archivos de configuración\",\r\n    \"15\": \"Guardar todos los cambios\",\r\n    \"16\": \"Solicitud de API de LLM en Chino\",\r\n    \"17\": \"Solicitar a través de la plataforma Volcengine:\",\r\n    \"18\": \"Enlace de solicitud: <a href=\\\"https://www.volcengine.com/product/doubao/\\\" style=\\\"color: #0066cc; text-decoration: none;\\\">Volcano-Doubao</a>\",\r\n    \"19\": \"Modelos compatibles: Doubao, serie Deepseek\",\r\n    \"20\": \"Solicitar a través de la plataforma Alibaba Cloud:\",\r\n    \"21\": \"Enlace de solicitud: <a href=\\\"https://cn.aliyun.com/product/tongyi?from_alibabacloud=&utm_content=se_1019997984\\\" style=\\\"color: #0066cc; text-decoration: none;\\\">Alicloud-Qwen</a>\",\r\n    \"22\": \"Modelos compatibles: Qwen-Max, Qwen-Plus, y otras series\",\r\n    \"23\": \"conteo:\",\r\n    \"24\": \"PPC:\",\r\n    \"25\": \"API de modelos de traducción\",\r\n    \"26\": \"Servicios de OCR\",\r\n    \"27\": \"Configuración predeterminada\",\r\n    \"28\": \"Agrega literatura a tu lista de lectura\",\r\n    \"29\": \"Arrastra y suelta tus archivos aquí o <span class=\\\"upload-link\\\" onclick=\\\"triggerFileInput()\\\"> haz clic para cargar</span>\",\r\n    \"30\": \"Hasta 12 archivos a la vez, cada uno con un máximo de 200MB\",\r\n    \"31\": \"Soporte: PDF, XPS, EPUB, FB2, CBZ, MOBI\",\r\n    \"32\": \"Por favor espera con paciencia, puedes revisar el progreso de la traducción en la pestaña de lectura reciente.\",\r\n    \"33\": \"Configuración de la API de traducción\",\r\n    \"34\": \"Openai API\",\r\n    \"35\": \"API de traducción de Doubao\",\r\n    \"36\": \"API de traducción de Qwen\",\r\n    \"37\": \"API de Deepseek\",\r\n    \"38\": \"API de deepl\",\r\n    \"39\": \"API de traducción de Youdao\",\r\n    \"40\": \"Modo global de OCR desactivado por defecto\",\r\n    \"41\": \"Modo de traducción habilitado\",\r\n    \"42\": \"Gestión por lotes\",\r\n    \"43\": \"Seleccionar todo\",\r\n    \"44\": \"Eliminar\",\r\n    \"45\": \"Mapa mental\",\r\n    \"46\": \"Resumen\",\r\n  \"47\": \"Notas:\\n\\n<ul>\\n<li>Los modos OCR y Line están desactivados por defecto\\n <ul> PPC (Páginas por llamada)\\n <li>OCR se utiliza para escanear documentos PDF basados en imágenes</li>\\n <li>El modo Line se utiliza para traducir PDFs con contenido disperso</li>\\n </ul>\\n</li>\\n<li>La función de traducción está habilitada por defecto</li>\\n<li>Después de cambiar el modelo de traducción, ingrese el nombre y la clave correspondientes en las opciones de la API de traducción</li>\\n<li>\\n Line_demo: <a href=\\\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/Line-model-demo.pdf\\\" target=\\\"_blank\\\">Line-model-demo.pdf</a>\\n <br>\\n Line_zh: <a href=\\\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/Line-model-demo_zh.pdf\\\" target=\\\"_blank\\\">Line-model-demo_zh.pdf</a>\\n</li>\\n</ul>\",\r\n    \"48\": \"API de traducción de Grok\",\r\n    \"49\": \"API de terceros\",\r\n    \"50\": \"API de traducción de GLM\",\r\n    \"51\": \"API de traducción de Bing\",\r\n    \"52\": \"Prompt de Traducción\",\r\n    \"53\": \"API de 302.ai\"\r\n  },\r\n  \"fr\": {\r\n    \"1\": \"Bibliothèque\",\r\n    \"2\": \"Page d’accueil\",\r\n    \"3\": \"Lecture récente\",\r\n    \"4\": \"Étapes de configuration\",\r\n    \"5\": \"Recherche\",\r\n    \"6\": \"Dossiers\",\r\n    \"7\": \"Lecture récente\",\r\n    \"8\": \"Traitement par lot\",\r\n    \"9\": \"Paramètres\",\r\n    \"10\": \"+ Ajouter un article\",\r\n    \"11\": \"Chinois\",\r\n    \"12\": \"Articles au total :\",\r\n    \"13\": \"Voir tout >\",\r\n    \"14\": \"Éditeur de fichier de configuration\",\r\n    \"15\": \"Enregistrer toutes les modifications\",\r\n    \"16\": \"Demande d’API de LLM Chinois\",\r\n    \"17\": \"Postuler via la plateforme Volcengine :\",\r\n    \"18\": \"Lien de demande : <a href=\\\"https://www.volcengine.com/product/doubao/\\\" style=\\\"color: #0066cc; text-decoration: none;\\\">Volcano-Doubao</a>\",\r\n    \"19\": \"Modèles pris en charge : Doubao, Deepseek et séries associées\",\r\n    \"20\": \"Postuler via la plateforme Alibaba Cloud :\",\r\n    \"21\": \"Lien de demande : <a href=\\\"https://cn.aliyun.com/product/tongyi?from_alibabacloud=&utm_content=se_1019997984\\\" style=\\\"color: #0066cc; text-decoration: none;\\\">Alicloud-Qwen</a>\",\r\n    \"22\": \"Modèles pris en charge : Qwen-Max, Qwen-Plus, et autres séries\",\r\n    \"23\": \"compteur :\",\r\n    \"24\": \"PPC :\",\r\n    \"25\": \"API de modèle de traduction\",\r\n    \"26\": \"Services OCR\",\r\n    \"27\": \"Configuration par défaut\",\r\n    \"28\": \"Ajouter des documents à votre liste de lecture\",\r\n    \"29\": \"Glissez-déposez vos fichiers ici ou <span class=\\\"upload-link\\\" onclick=\\\"triggerFileInput()\\\"> cliquez pour télécharger</span>\",\r\n    \"30\": \"Jusqu’à 12 fichiers à la fois, chacun de 200 Mo maximum\",\r\n    \"31\": \"Prise en charge : PDF, XPS, EPUB, FB2, CBZ, MOBI\",\r\n    \"32\": \"Veuillez patienter, vous pouvez consulter l’avancée de la traduction dans l’onglet lecture récente.\",\r\n    \"33\": \"Paramètres de l’API de traduction\",\r\n    \"34\": \"Openai API\",\r\n    \"35\": \"Doubao Translate API\",\r\n    \"36\": \"Qwen Translate API\",\r\n    \"37\": \"Deepseek API\",\r\n    \"38\": \"deepl API\",\r\n    \"39\": \"Youdao Translation API\",\r\n    \"40\": \"Mode OCR global désactivé par défaut\",\r\n    \"41\": \"Mode de traduction activé\",\r\n    \"42\": \"Gestion par lot\",\r\n    \"43\": \"Tout sélectionner\",\r\n    \"44\": \"Supprimer\",\r\n    \"45\": \"Carte mentale\",\r\n    \"46\": \"Résumé\",\r\n  \"47\": \"Remarques:\\n\\n<ul>\\n<li>Les modes OCR et Line sont désactivés par défaut\\n <ul> PPC (Pages par appel)\\n <li>OCR est utilisé pour la numérisation des PDF basés sur des images</li>\\n <li>Le mode Line est utilisé pour traduire des PDF au contenu clairsemé</li>\\n </ul>\\n</li>\\n<li>La fonction de traduction est activée par défaut</li>\\n<li>Après avoir changé le modèle de traduction, veuillez saisir le nom et la clé correspondants dans les options de l’API de traduction</li>\\n<li>\\n Line_demo: <a href=\\\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/Line-model-demo.pdf\\\" target=\\\"_blank\\\">Line-model-demo.pdf</a>\\n <br>\\n Line_zh: <a href=\\\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/Line-model-demo_zh.pdf\\\" target=\\\"_blank\\\">Line-model-demo_zh.pdf</a>\\n</li>\\n</ul>\",\r\n    \"48\": \"Grok Translate API\",\r\n    \"49\": \"API Tiers\",\r\n    \"50\": \"API de traduction GLM\",\r\n    \"51\": \"API de traduction Bing\",\r\n    \"52\": \"Prompt de Traduction\",\r\n    \"53\": \"API de 302.ai\"\r\n  },\r\n      \"de\": {\r\n    \"1\": \"Bibliothek\",\r\n    \"2\": \"Homepage\",\r\n    \"3\": \"Kürzlich gelesen\",\r\n    \"4\": \"Setup-Schritte\",\r\n    \"5\": \"Suche\",\r\n    \"6\": \"Ordner\",\r\n    \"7\": \"Kürzlich gelesen\",\r\n    \"8\": \"Stapel\",\r\n    \"9\": \"Einstellungen\",\r\n    \"10\": \"+ Artikel hinzufügen\",\r\n    \"11\": \"Chinesisch\",\r\n    \"12\": \"Artikel insgesamt:\",\r\n    \"13\": \"Alle anzeigen >\",\r\n    \"14\": \"Konfigurationsdatei-Editor\",\r\n    \"15\": \"Alle Änderungen speichern\",\r\n    \"16\": \"Chinese LLM API-Anwendung\",\r\n    \"17\": \"Antrag über Volcengine-Plattform:\",\r\n    \"18\": \"Antragslink: <a href=\\\"https://www.volcengine.com/product/doubao/\\\" style=\\\"color: #0066cc; text-decoration: none;\\\">Volcano-Doubao</a>\",\r\n    \"19\": \"Unterstützte Modelle: Doubao, Deepseek Serienmodelle\",\r\n    \"20\": \"Antrag über Alibaba-Cloud-Plattform:\",\r\n    \"21\": \"Antragslink: <a href=\\\"https://cn.aliyun.com/product/tongyi?from_alibabacloud=&utm_content=se_1019997984\\\" style=\\\"color: #0066cc; text-decoration: none;\\\">Alicloud-Qwen</a>\",\r\n    \"22\": \"Unterstützte Modelle: Qwen-Max, Qwen-Plus und andere Serien\",\r\n    \"23\": \"Anzahl:\",\r\n    \"24\": \"PPC:\",\r\n    \"25\": \"Übersetzungsmodell-API\",\r\n    \"26\": \"OCR-Dienste\",\r\n    \"27\": \"Standardkonfiguration\",\r\n    \"28\": \"Fügen Sie Literatur zu Ihrer Leseliste hinzu\",\r\n    \"29\": \"Ziehen Sie Ihre Dateien hierher oder <span class=\\\"upload-link\\\" onclick=\\\"triggerFileInput()\\\">klicken Sie zum Hochladen</span>\",\r\n    \"30\": \"Bis zu 12 Dateien gleichzeitig, jede max. 200MB\",\r\n    \"31\": \"Unterstützt: PDF, XPS, EPUB, FB2, CBZ, MOBI\",\r\n    \"32\": \"Bitte warten Sie einen Moment. Den Fortschritt der Übersetzung können Sie unter dem Reiter „Kürzlich gelesen“ verfolgen.\",\r\n    \"33\": \"Einstellungen der Übersetzungs-API\",\r\n    \"34\": \"Openai API\",\r\n    \"35\": \"Doubao Translate API\",\r\n    \"36\": \"Qwen Translate API\",\r\n    \"37\": \"Deepseek API\",\r\n    \"38\": \"deepl API\",\r\n    \"39\": \"Youdao Translation API\",\r\n    \"40\": \"OCR-Globalmodus standardmäßig deaktiviert\",\r\n    \"41\": \"Übersetzungsmodus aktiviert\",\r\n    \"42\": \"Stapelverwaltung\",\r\n    \"43\": \"Alles auswählen\",\r\n    \"44\": \"Löschen\",\r\n    \"45\": \"Mind Map\",\r\n    \"46\": \"Zusammenfassung\",\r\n  \"47\": \"Hinweise:\\n\\n<ul>\\n<li>OCR- und Line-Modus sind standardmäßig deaktiviert\\n <ul> PPC (Seiten pro Aufruf)\\n <li>OCR wird zum Scannen bildbasierter PDFs verwendet</li>\\n <li>Der Line-Modus wird für die Übersetzung spärlicher PDFs verwendet</li>\\n </ul>\\n</li>\\n<li>Die Übersetzungsfunktion ist standardmäßig aktiviert</li>\\n<li>Nach dem Wechsel des Übersetzungsmodells geben Sie bitte den entsprechenden Modellnamen und Schlüssel in den Übersetzungs-API-Optionen ein</li>\\n<li>\\n Line_demo: <a href=\\\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/Line-model-demo.pdf\\\" target=\\\"_blank\\\">Line-model-demo.pdf</a>\\n <br>\\n Line_zh: <a href=\\\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/Line-model-demo_zh.pdf\\\" target=\\\"_blank\\\">Line-model-demo_zh.pdf</a>\\n</li>\\n</ul>\",\r\n    \"48\": \"Grok Translate API\",\r\n    \"49\": \"Drittanbieter-API\",\r\n    \"50\": \"GLM Übersetzer-API\",\r\n    \"51\": \"Bing Übersetzer-API\",\r\n    \"52\": \"Übersetzungs-Prompt\",\r\n    \"53\": \"302.ai API\"\r\n      },\r\n  \"ar\": {\r\n    \"1\": \"المكتبة\",\r\n    \"2\": \"الصفحة الرئيسية\",\r\n    \"3\": \"آخر القراءة\",\r\n    \"4\": \"خطوات الإعداد\",\r\n    \"5\": \"بحث\",\r\n    \"6\": \"مجلدات\",\r\n    \"7\": \"آخر القراءة\",\r\n    \"8\": \"دفعة\",\r\n    \"9\": \"الإعدادات\",\r\n    \"10\": \"+ إضافة مقال\",\r\n    \"11\": \"الصينية\",\r\n    \"12\": \"إجمالي المقالات:\",\r\n    \"13\": \"عرض الكل >\",\r\n    \"14\": \"محرر ملف الإعداد\",\r\n    \"15\": \"حفظ جميع التغييرات\",\r\n    \"16\": \"تقديم طلب واجهة برمجة تطبيقات LLM الصينية\",\r\n    \"17\": \"التقديم عبر منصة Volcengine:\",\r\n    \"18\": \"رابط التقديم: <a href=\\\"https://www.volcengine.com/product/doubao/\\\" style=\\\"color: #0066cc; text-decoration: none;\\\">Volcano-Doubao</a>\",\r\n    \"19\": \"النماذج المدعومة: Doubao، سلسلة Deepseek\",\r\n    \"20\": \"التقديم عبر منصة Alibaba Cloud:\",\r\n    \"21\": \"رابط التقديم: <a href=\\\"https://cn.aliyun.com/product/tongyi?from_alibabacloud=&utm_content=se_1019997984\\\" style=\\\"color: #0066cc; text-decoration: none;\\\">Alicloud-Qwen</a>\",\r\n    \"22\": \"النماذج المدعومة: Qwen-Max، Qwen-Plus، وسلاسل أخرى\",\r\n    \"23\": \"العدد:\",\r\n    \"24\": \"PPC:\",\r\n    \"25\": \"واجهة برمجة تطبيقات نموذج الترجمة\",\r\n    \"26\": \"خدمات OCR\",\r\n    \"27\": \"الإعداد الافتراضي\",\r\n    \"28\": \"أضف الأدبيات إلى قائمة القراءة الخاصة بك\",\r\n    \"29\": \"اسحب ملفاتك إلى هنا أو <span class=\\\"upload-link\\\" onclick=\\\"triggerFileInput()\\\">انقر للتحميل</span>\",\r\n    \"30\": \"يُسمح بتحميل حتى 12 ملفًا دفعة واحدة، حجم كل منها يصل إلى 200 ميجابايت\",\r\n    \"31\": \"مدعوم: PDF, XPS, EPUB, FB2, CBZ, MOBI\",\r\n    \"32\": \"يرجى الانتظار بصبر. يمكنك التحقق من تقدم الترجمة في علامة التبويب (آخر القراءة).\",\r\n    \"33\": \"إعدادات واجهة برمجة تطبيقات الترجمة\",\r\n    \"34\": \"Openai API\",\r\n    \"35\": \"Doubao Translate API\",\r\n    \"36\": \"Qwen Translate API\",\r\n    \"37\": \"Deepseek API\",\r\n    \"38\": \"deepl API\",\r\n    \"39\": \"Youdao Translation API\",\r\n    \"40\": \"وضع OCR العالمي معطل افتراضيًا\",\r\n    \"41\": \"تم تفعيل وضع الترجمة\",\r\n    \"42\": \"إدارة الدفعات\",\r\n    \"43\": \"تحديد الكل\",\r\n    \"44\": \"حذف\",\r\n    \"45\": \"خريطة ذهنية\",\r\n    \"46\": \"ملخص\",\r\n    \"47\": \"ملاحظات:\\n\\n<ul>\\n<li>يتم تعطيل وضعي OCR وLine افتراضيًا\\n <ul> PPC (عدد الصفحات لكل طلب)\\n <li>يُستخدم OCR لمسح ملفات PDF المُستنِدة إلى الصور ضوئيًا</li>\\n <li>يُستخدم وضع Line لترجمة ملفات PDF ذات المحتوى القليل</li>\\n </ul>\\n</li>\\n<li>ميزة الترجمة مفعّلة افتراضيًا</li>\\n<li>بعد تغيير نموذج الترجمة، يرجى إدخال اسم النموذج والمفتاح المناسبين في خيارات واجهة برمجة تطبيقات الترجمة</li>\\n<li>\\n Line_demo: <a href=\\\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/Line-model-demo.pdf\\\" target=\\\"_blank\\\">Line-model-demo.pdf</a>\\n <br>\\n Line_zh: <a href=\\\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/Line-model-demo_zh.pdf\\\" target=\\\"_blank\\\">Line-model-demo_zh.pdf</a>\\n</li>\\n</ul>\",\r\n    \"48\": \"Grok Translate API\",\r\n    \"49\": \"واجهة برمجة تطبيقات الطرف الثالث\",\r\n    \"50\": \"واجهة برمجة تطبيقات ترجمة GLM\",\r\n    \"51\": \"واجهة برمجة تطبيقات ترجمة Bing\",\r\n    \"52\": \"موجه الترجمة\",\r\n    \"53\": \"واجهة برمجة تطبيقات 302.ai\"\r\n  },\r\n  \"ja\": {\r\n    \"1\": \"ライブラリ\",\r\n    \"2\": \"ホームページ\",\r\n    \"3\": \"最近の読書\",\r\n    \"4\": \"設定手順\",\r\n    \"5\": \"検索\",\r\n    \"6\": \"フォルダー\",\r\n    \"7\": \"最近の読書\",\r\n    \"8\": \"一括\",\r\n    \"9\": \"設定\",\r\n    \"10\": \"+ 記事を追加\",\r\n    \"11\": \"中国語\",\r\n    \"12\": \"記事の総数：\",\r\n    \"13\": \"すべて見る >\",\r\n    \"14\": \"設定ファイルエディター\",\r\n    \"15\": \"すべての変更を保存\",\r\n    \"16\": \"中国語LLM API申請\",\r\n    \"17\": \"Volcengineプラットフォームを介して申請：\",\r\n    \"18\": \"申請リンク：<a href=\\\"https://www.volcengine.com/product/doubao/\\\" style=\\\"color: #0066cc; text-decoration: none;\\\">Volcano-Doubao</a>\",\r\n    \"19\": \"対応モデル：Doubao、Deepseek シリーズ\",\r\n    \"20\": \"Alibaba Cloudプラットフォームを介して申請：\",\r\n    \"21\": \"申請リンク：<a href=\\\"https://cn.aliyun.com/product/tongyi?from_alibabacloud=&utm_content=se_1019997984\\\" style=\\\"color: #0066cc; text-decoration: none;\\\">Alicloud-Qwen</a>\",\r\n    \"22\": \"対応モデル：Qwen-Max、Qwen-Plus、およびその他のシリーズ\",\r\n    \"23\": \"カウント：\",\r\n    \"24\": \"PPC：\",\r\n    \"25\": \"翻訳モデル API\",\r\n    \"26\": \"OCR サービス\",\r\n    \"27\": \"デフォルト設定\",\r\n    \"28\": \"文献を読書リストに追加\",\r\n    \"29\": \"ファイルをここにドラッグ＆ドロップまたは<span class=\\\"upload-link\\\" onclick=\\\"triggerFileInput()\\\">クリックしてアップロード</span>\",\r\n    \"30\": \"一度に最大12ファイル、ファイルサイズは200MBまで\",\r\n    \"31\": \"対応：PDF, XPS, EPUB, FB2, CBZ, MOBI\",\r\n    \"32\": \"しばらくお待ちください。「最近の読書」タブで翻訳進捗を確認できます。\",\r\n    \"33\": \"翻訳 API 設定\",\r\n    \"34\": \"Openai API\",\r\n    \"35\": \"Doubao 翻訳 API\",\r\n    \"36\": \"Qwen 翻訳 API\",\r\n    \"37\": \"Deepseek API\",\r\n    \"38\": \"deepl API\",\r\n    \"39\": \"Youdao 翻訳 API\",\r\n    \"40\": \"OCR グローバルモードはデフォルトで無効\",\r\n    \"41\": \"翻訳モードが有効\",\r\n    \"42\": \"一括管理\",\r\n    \"43\": \"すべて選択\",\r\n    \"44\": \"削除\",\r\n    \"45\": \"マインドマップ\",\r\n    \"46\": \"要約\",\r\n  \"47\": \"注釈:\\n\\n<ul>\\n<li>OCR と Line モードはデフォルトで無効になっています\\n <ul> PPC（1回の呼び出しで処理できるページ数）\\n <li>OCR は画像ベースの PDF をスキャンするために使用します</li>\\n <li>Line モードは内容が疎な PDF を翻訳するために使用します</li>\\n </ul>\\n</li>\\n<li>翻訳機能はデフォルトで有効です</li>\\n<li>翻訳モデルを変更した後は、対応するモデル名とキーを翻訳モデル API のオプションに入力してください</li>\\n<li>\\n Line_demo: <a href=\\\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/Line-model-demo.pdf\\\" target=\\\"_blank\\\">Line-model-demo.pdf</a>\\n <br>\\n Line_zh: <a href=\\\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/Line-model-demo_zh.pdf\\\" target=\\\"_blank\\\">Line-model-demo_zh.pdf</a>\\n</li>\\n</ul>\",\r\n    \"48\": \"Grok 翻訳 API\",\r\n    \"49\": \"サードパーティ API\",\r\n    \"50\": \"智谱 GLM 翻訳 API\",\r\n    \"51\": \"Bing 翻訳 API\",\r\n    \"52\": \"翻訳プロンプト\",\r\n    \"53\": \"302.ai API\"\r\n  },\r\n  \"ko\": {\r\n    \"1\": \"라이브러리\",\r\n    \"2\": \"홈페이지\",\r\n    \"3\": \"최근 읽기\",\r\n    \"4\": \"설정 단계\",\r\n    \"5\": \"검색\",\r\n    \"6\": \"폴더\",\r\n    \"7\": \"최근 읽기\",\r\n    \"8\": \"일괄\",\r\n    \"9\": \"설정\",\r\n    \"10\": \"+ 기사 추가\",\r\n    \"11\": \"중국어\",\r\n    \"12\": \"총 기사 수:\",\r\n    \"13\": \"전체 보기 >\",\r\n    \"14\": \"설정 파일 편집기\",\r\n    \"15\": \"모든 변경 내용 저장\",\r\n    \"16\": \"중국어 LLM API 신청\",\r\n    \"17\": \"Volcengine 플랫폼을 통해 신청:\",\r\n    \"18\": \"신청 링크: <a href=\\\"https://www.volcengine.com/product/doubao/\\\" style=\\\"color: #0066cc; text-decoration: none;\\\">Volcano-Doubao</a>\",\r\n    \"19\": \"지원 모델: Doubao, Deepseek 시리즈\",\r\n    \"20\": \"Alibaba Cloud 플랫폼을 통해 신청:\",\r\n    \"21\": \"신청 링크: <a href=\\\"https://cn.aliyun.com/product/tongyi?from_alibabacloud=&utm_content=se_1019997984\\\" style=\\\"color: #0066cc; text-decoration: none;\\\">Alicloud-Qwen</a>\",\r\n    \"22\": \"지원 모델: Qwen-Max, Qwen-Plus 등\",\r\n    \"23\": \"개수:\",\r\n    \"24\": \"PPC:\",\r\n    \"25\": \"번역 모델 API\",\r\n    \"26\": \"OCR 서비스\",\r\n    \"27\": \"기본 설정\",\r\n    \"28\": \"문헌을 읽기 목록에 추가\",\r\n    \"29\": \"파일을 여기로 드래그 앤 드롭하거나 <span class=\\\"upload-link\\\" onclick=\\\"triggerFileInput()\\\">클릭하여 업로드</span>\",\r\n    \"30\": \"최대 12개 파일 업로드 가능, 파일당 최대 200MB\",\r\n    \"31\": \"지원 형식: PDF, XPS, EPUB, FB2, CBZ, MOBI\",\r\n    \"32\": \"잠시 기다려주세요. '최근 읽기' 탭에서 번역 진행 상황을 확인하실 수 있습니다.\",\r\n    \"33\": \"번역 API 설정\",\r\n    \"34\": \"Openai API\",\r\n    \"35\": \"Doubao 번역 API\",\r\n    \"36\": \"Qwen 번역 API\",\r\n    \"37\": \"Deepseek API\",\r\n    \"38\": \"deepl API\",\r\n    \"39\": \"Youdao 번역 API\",\r\n    \"40\": \"OCR 글로벌 모드는 기본적으로 비활성화됨\",\r\n    \"41\": \"번역 모드가 활성화됨\",\r\n    \"42\": \"일괄 관리\",\r\n    \"43\": \"전체 선택\",\r\n    \"44\": \"삭제\",\r\n    \"45\": \"마인드 맵\",\r\n    \"46\": \"요약\",\r\n  \"47\": \"참고사항:\\n\\n<ul>\\n<li>OCR 모드와 Line 모드는 기본적으로 비활성화되어 있습니다\\n <ul> PPC (호출 당 처리할 수 있는 페이지 수)\\n <li>OCR은 이미지 기반 PDF 스캔에 사용됩니다</li>\\n <li>Line 모드는 내용이 적은 PDF를 번역하는 데 사용됩니다</li>\\n </ul>\\n</li>\\n<li>번역 기능은 기본적으로 활성화되어 있습니다</li>\\n<li>번역 모델을 변경한 후에는 해당 모델 이름 및 키를 번역 모델 API 옵션에 입력해주세요</li>\\n<li>\\n Line_demo: <a href=\\\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/Line-model-demo.pdf\\\" target=\\\"_blank\\\">Line-model-demo.pdf</a>\\n <br>\\n Line_zh: <a href=\\\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/Line-model-demo_zh.pdf\\\" target=\\\"_blank\\\">Line-model-demo_zh.pdf</a>\\n</li>\\n</ul>\",\r\n    \"48\": \"Grok 번역 API\",\r\n    \"49\": \"서드파티 API\",\r\n    \"50\": \"GLM 번역 API\",\r\n    \"51\": \"Bing 번역 API\",\r\n    \"52\": \"번역 프롬프트\",\r\n    \"53\": \"302.ai API\"\r\n  },\r\n  \"ru\": {\r\n    \"1\": \"Библиотека\",\r\n    \"2\": \"Главная страница\",\r\n    \"3\": \"Недавние чтения\",\r\n    \"4\": \"Шаги настройки\",\r\n    \"5\": \"Поиск\",\r\n    \"6\": \"Папки\",\r\n    \"7\": \"Недавние чтения\",\r\n    \"8\": \"Пакетная обработка\",\r\n    \"9\": \"Настройки\",\r\n    \"10\": \"+ Добавить статью\",\r\n    \"11\": \"Китайский\",\r\n    \"12\": \"Всего статей:\",\r\n    \"13\": \"Просмотреть все >\",\r\n    \"14\": \"Редактор файла конфигурации\",\r\n    \"15\": \"Сохранить все изменения\",\r\n    \"16\": \"Заявка на китайский LLM API\",\r\n    \"17\": \"Подать заявку через платформу Volcengine:\",\r\n    \"18\": \"Ссылка для заявки: <a href=\\\"https://www.volcengine.com/product/doubao/\\\" style=\\\"color: #0066cc; text-decoration: none;\\\">Volcano-Doubao</a>\",\r\n    \"19\": \"Поддерживаемые модели: Doubao, серия Deepseek\",\r\n    \"20\": \"Подать заявку через платформу Alibaba Cloud:\",\r\n    \"21\": \"Ссылка для заявки: <a href=\\\"https://cn.aliyun.com/product/tongyi?from_alibabacloud=&utm_content=se_1019997984\\\" style=\\\"color: #0066cc; text-decoration: none;\\\">Alicloud-Qwen</a>\",\r\n    \"22\": \"Поддерживаемые модели: Qwen-Max, Qwen-Plus и другие серии\",\r\n    \"23\": \"Количество:\",\r\n    \"24\": \"PPC:\",\r\n    \"25\": \"API модели перевода\",\r\n    \"26\": \"OCR-сервисы\",\r\n    \"27\": \"Настройки по умолчанию\",\r\n    \"28\": \"Добавьте литературу в свой список для чтения\",\r\n    \"29\": \"Перетащите файлы сюда или <span class=\\\"upload-link\\\" onclick=\\\"triggerFileInput()\\\">щёлкните, чтобы загрузить</span>\",\r\n    \"30\": \"До 12 файлов за раз, каждый — максимум 200 МБ\",\r\n    \"31\": \"Поддерживаются: PDF, XPS, EPUB, FB2, CBZ, MOBI\",\r\n    \"32\": \"Пожалуйста, подождите. Вы можете отслеживать прогресс перевода во вкладке «Недавние чтения».\",\r\n    \"33\": \"Настройки API перевода\",\r\n    \"34\": \"Openai API\",\r\n    \"35\": \"Doubao Translate API\",\r\n    \"36\": \"Qwen Translate API\",\r\n    \"37\": \"Deepseek API\",\r\n    \"38\": \"deepl API\",\r\n    \"39\": \"Youdao Translation API\",\r\n    \"40\": \"Глобальный режим OCR по умолчанию отключён\",\r\n    \"41\": \"Режим перевода включён\",\r\n    \"42\": \"Пакетное управление\",\r\n    \"43\": \"Выбрать все\",\r\n    \"44\": \"Удалить\",\r\n    \"45\": \"Mind Map\",\r\n    \"46\": \"Резюме\",\r\n  \"47\": \"Примечания:\\n\\n<ul>\\n<li>Режимы OCR и Line по умолчанию отключены\\n <ul> PPC (количество страниц за вызов)\\n <li>OCR используется для сканирования PDF-файлов, основанных на изображениях</li>\\n <li>Режим Line используется для перевода PDF-файлов с разреженным содержимым</li>\\n </ul>\\n</li>\\n<li>Функция перевода включена по умолчанию</li>\\n<li>После смены модели перевода в настройках API необходимо ввести соответствующее название модели и ключ</li>\\n<li>\\n Line_demo: <a href=\\\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/Line-model-demo.pdf\\\" target=\\\"_blank\\\">Line-model-demo.pdf</a>\\n <br>\\n Line_zh: <a href=\\\"https://github.com/CBIhalsen/PolyglotPDF/blob/main/static/Line-model-demo_zh.pdf\\\" target=\\\"_blank\\\">Line-model-demo_zh.pdf</a>\\n</li>\\n</ul>\",\r\n    \"48\": \"Grok Translate API\",\r\n    \"49\": \"API третьих сторон\",\r\n    \"50\": \"GLM API перевода\",\r\n    \"51\": \"Bing API перевода\",\r\n    \"52\": \"Промпт перевода\",\r\n    \"53\": \"302.ai API\"\r\n  }\r\n\r\n};"
  },
  {
    "path": "static/main.css",
    "content": "\n        * {\n            margin: 0;\n            padding: 0;\n            box-sizing: border-box;\n        }\n\n        body {\n            font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n            background-color: #f5f5f5;\n        }\n\n        .container {\n            display: flex;\n            min-height: 100vh;\n        }\n\n        .sidebar {\n            width: 240px;\n            background-color: white;\n            padding: 20px;\n            position: fixed;\n            height: 100vh;\n        }\n\n        .sidebar-menu {\n            list-style: none;\n        }\n\n        .sidebar-menu li {\n            margin: 10px 0;\n        }\n\n        .sidebar-menu a {\n            text-decoration: none;\n            color: #666;\n            display: block;\n            padding: 10px;\n            border-radius: 5px;\n            transition: all 0.3s;\n        }\n\n        .sidebar-menu a.active {\n            background-color: #6366f1;\n            color: white;\n        }\n\n        .main-content {\n            flex: 1;\n            padding: 20px;\n            margin-left: 240px;\n        }\n\n        .header {\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            margin-bottom: 30px;\n        }\n\n        .header-buttons {\n            display: flex;\n            gap: 10px;\n            align-items: center;\n        }\n\n        .batch-button,\n.settings-content {\n    position: relative;\n    padding: 30px;\n}\n\n.settings-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    margin-bottom: 20px;\n}\n\n.close-btn {\n    background: none;\n    border: none;\n    font-size: 24px;\n    cursor: pointer;\n    padding: 5px 10px;\n    color: #666;\n}\n\n.close-btn:hover {\n    color: #000;\n}\n\n\n\n.api-selection {\n    margin: 20px 0;\n}\n\n.api-option {\n    margin: 15px 0;\n}\n\n\n\n\n.settings-footer {\n    text-align: right;\n    margin-top: 20px;\n}\n\n.save-btn {\n    background-color: #6366f1;\n    color: white;\n    border: none;\n    padding: 10px 20px;\n    border-radius: 5px;\n    cursor: pointer;\n    transition: background-color 0.3s;\n}\n\n.save-btn:hover {\n    background-color: #4f46e5;\n}\n\n.save-btn.success {\n    background-color: #22c55e;\n}\n\n        .recent-header {\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            margin-bottom: 20px;\n        }\n\n        .view-all {\n            color: #6366f1;\n            text-decoration: none;\n            font-size: 14px;\n        }\n\n        .view-all:hover {\n            text-decoration: underline;\n        }\n\n        .add-button {\n            background-color: #6366f1;\n            color: white;\n            border: none;\n            padding: 10px 20px;\n            border-radius: 5px;\n            cursor: pointer;\n        }\n\n        .article-grid {\n            display: grid;\n            grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));\n            gap: 20px;\n        }\n\n      .article-card {\n    background-color: white;\n    border-radius: 10px;\n    padding: 20px;\n    box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n    position: relative;\n    text-decoration: none;\n    color: inherit;\n    display: block;\n    transition: all 0.3s ease;\n}\n\n.article-card:hover {\n    box-shadow: 0 4px 8px rgba(0,0,0,0.2);\n}\n\n.article-top img {\n    width: 100%;\n    height: 200px;\n    object-fit: cover;\n    border-radius: 5px;\n}\n\n.article-bottom {\n    margin-top: 15px;\n    position: relative;\n}\n\n.article-title h3 {\n    margin: 0 0 10px 0;\n    font-size: 18px;\n}\n\n.article-info {\n    display: flex;\n    gap: 15px;\n    color: #666;\n    font-size: 14px;\n}\n\n.status-indicator {\n    position: absolute;\n    top: 10px;\n    right: 10px;\n}\n\n.read-status {\n    position: absolute;\n    top: 10px;\n    right: 40px;\n    padding: 4px 8px;\n    border-radius: 4px;\n    font-size: 12px;\n    display: none;\n}\n\n.article-card:hover .read-status {\n    display: block;\n}\n\n.unread {\n    background-color: #ff4d4f;\n    color: white;\n}\n\n.read {\n    background-color: #52c41a;\n    color: white;\n}\n\n.menu-button {\n    position: absolute;\n    top: 10px;\n    right: 10px;\n    background: none;\n    border: none;\n    cursor: pointer;\n    padding: 5px;\n}\n\n\n.article-menu {\n    background: white;\n    border-radius: 4px;\n    box-shadow: 0 2px 8px rgba(0,0,0,0.15);\n    z-index: 1000;\n    transform: translateX(-80px);  /* 向左位移20px */\n    width: 80px;\n}\n\n\n.menu-item {\n    padding: 8px 16px;\n    cursor: pointer;\n}\n\n.menu-item:hover {\n    background-color: #f5f5f5;\n}\n\n\n        .upload-container,\n        .settings-container {\n            display: none;\n            position: fixed;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background-color: rgba(0,0,0,0.5);\n            z-index: 1000;\n        }\n\n        .upload-content,\n        .settings-content {\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            background-color: white;\n            padding: 30px;\n            border-radius: 10px;\n            width: 80%;\n            max-width: 800px;\n        }\n\n        .settings-content {\n            max-width: 400px;\n        }\n\n        .settings-option {\n            margin: 20px 0;\n        }\n\n        .settings-option label {\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            margin: 10px 0;\n        }\n\n        .toggle-switch {\n            position: relative;\n            display: inline-block;\n            width: 40px;\n            height: 20px;\n        }\n\n        .toggle-switch input {\n            opacity: 0;\n            width: 0;\n            height: 0;\n        }\n\n        .slider {\n            position: absolute;\n            cursor: pointer;\n            top: 0;\n            left: 0;\n            right: 0;\n            bottom: 0;\n            background-color: #ccc;\n            transition: .4s;\n            border-radius: 20px;\n        }\n\n        .slider:before {\n            position: absolute;\n            content: \"\";\n            height: 16px;\n            width: 16px;\n            left: 2px;\n            bottom: 2px;\n            background-color: white;\n            transition: .4s;\n            border-radius: 50%;\n        }\n\n        input:checked + .slider {\n            background-color: #6366f1;\n        }\n\n        input:checked + .slider:before {\n            transform: translateX(20px);\n        }\n\n.upload-area {\n    border: 2px dashed #e0e0e0;\n    border-radius: 8px;\n    padding: 40px 20px;\n    text-align: center;\n    background-color: #fafafa;\n    cursor: pointer;\n    transition: all 0.3s ease;\n}\n\n.upload-area:hover {\n    border-color: #1a73e8;\n    background-color: #f8f9fe;\n}\n\n.upload-area i {\n    font-size: 48px;\n    color: #666;\n    margin-bottom: 16px;\n}\n\n.upload-text {\n    font-size: 16px;\n    color: #333;\n    margin: 12px 0;\n}\n\n.upload-link {\n    color: #1a73e8;\n    cursor: pointer;\n    text-decoration: none;\n}\n\n.upload-link:hover {\n    text-decoration: underline;\n}\n\n.upload-hint {\n    font-size: 14px;\n    color: #666;\n    margin: 8px 0;\n}\n\n.upload-formats {\n    font-size: 14px;\n    color: #666;\n    margin: 8px 0;\n}\n\n        .loading {\n            text-align: center;\n            padding: 20px;\n            color: #666;\n        }\n\n        .error {\n            text-align: center;\n            padding: 20px;\n            color: #dc2626;\n        }\n        .upload-files-list {\n    margin: 20px 0;\n    max-height: 300px;\n    overflow-y: auto;\n}\n\n.upload-file-item {\n    display: flex;\n    align-items: center;\n    padding: 10px;\n    background: #f5f5f5;\n    border-radius: 5px;\n    margin-bottom: 10px;\n}\n\n.upload-file-info {\n    flex: 1;\n    margin-right: 10px;\n}\n\n.upload-file-name {\n    font-weight: 500;\n    margin-bottom: 5px;\n}\n\n.upload-file-meta {\n    font-size: 12px;\n    color: #666;\n}\n\n.upload-status {\n    font-size: 12px;\n    padding: 4px 8px;\n    border-radius: 3px;\n}\n\n.status-pending {\n    background: #e5e7eb;\n}\n\n.status-uploading {\n    background: #dbeafe;\n    color: #2563eb;\n}\n\n.status-success {\n    background: #dcfce7;\n    color: #16a34a;\n}\n\n.status-error {\n    background: #fee2e2;\n    color: #dc2626;\n}\n\n.upload-link {\n    color: #6366f1;\n    cursor: pointer;\n}\n\n.upload-link:hover {\n    text-decoration: underline;\n}\n\n.upload-actions {\n    display: flex;\n    justify-content: flex-end;\n    gap: 10px;\n    margin-top: 20px;\n}\n.language-selection {\n    margin-top: 20px;\n    padding-top: 20px;\n    border-top: 2px dashed #e5e7eb;\n    display: flex;\n    gap: 20px;\n}\n\n.source-lang, .target-lang {\n    flex: 1;\n}\n\nselect {\n    width: 100%;\n    padding: 8px;\n    border: 1px solid #d1d5db;\n    border-radius: 4px;\n    font-size: 14px;\n}\n.next-step-container {\n    margin-top: 20px;\n    text-align: center;\n}\n\n.next-step-btn {\n    padding: 10px 30px;\n    background-color: #4CAF50;\n    color: white;\n    border: none;\n    border-radius: 4px;\n    cursor: pointer;\n    font-size: 16px;\n}\n\n.next-step-btn:hover {\n    background-color: #45a049;\n}\n\n.success-message {\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n    text-align: center;\n}\n\n.success-icon {\n    font-size: 80px;\n    color: #4CAF50;\n    margin-bottom: 20px;\n}\n\n.success-text {\n    font-size: 18px;\n    color: #333;\n}\n\n.ocr-toggle {\n    padding: 15px;\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    border-bottom: 1px solid #eee;\n}\n\n.switch {\n    position: relative;\n    display: inline-block;\n    width: 50px;\n    height: 24px;\n}\n\n.switch input {\n    opacity: 0;\n    width: 0;\n    height: 0;\n}\n\n.slider {\n    position: absolute;\n    cursor: pointer;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    background-color: #ccc;\n    transition: .4s;\n}\n\n.slider:before {\n    position: absolute;\n    content: \"\";\n    height: 20px;\n    width: 20px;\n    left: 2px;\n    bottom: 2px;\n    background-color: white;\n    transition: .4s;\n}\n\n.slider.round {\n    border-radius: 24px;\n}\n\n.slider.round:before {\n    border-radius: 50%;\n}\n\ninput:checked + .slider {\n    background-color: #6366f1; /* 使用图片中的紫色 */\n}\n\ninput:checked + .slider:before {\n    transform: translateX(26px);\n}\n\n/* CSS */\n.button-group {\n    display: flex;\n    gap: 8px;\n}\n\n.action-button {\n    display: flex;\n    align-items: center;\n    gap: 4px;\n    padding: 6px 12px;\n    border: 1px solid #e0e0e0;\n    border-radius: 4px;\n    background-color: white;\n    color: #666;\n    cursor: pointer;\n    font-size: 14px;\n}\n\n.action-button:hover {\n    background-color: #f5f5f5;\n}\n\n.action-button i {\n    font-size: 14px;\n}\n\n.action-button span {\n    vertical-align: middle;\n}\n#batchModal {\n  display: none; /* 初始隐藏，与原有modal逻辑一致 */\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  background-color: rgba(0,0,0,0.5);\n  z-index: 2000; /* 设大一点以免被其他遮罩挡住 */\n}\n\n/* 弹窗内容区域 */\n.batch-content {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  background-color: white;\n  padding: 20px;\n  border-radius: 10px;\n  width: 80%;\n  max-width: 900px;\n  max-height: 80%;\n  overflow: hidden; /* 超出部分滚动 */\n  display: flex;\n  flex-direction: column;\n}\n\n/* 弹窗头部 */\n.batch-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  margin-bottom: 10px;\n}\n\n.batch-header h2 {\n  margin: 0;\n}\n\n.batch-controls {\n  display: flex;\n  gap: 10px;\n  margin-bottom: 10px;\n}\n\n/* 卡片区域 */\n.batch-grid {\n  flex: 1; /* 占满剩余可用空间 */\n  overflow-y: auto;\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n  gap: 10px;\n}\n\n/* 卡片本身 */\n.batch-card {\n  background-color: #fff;\n  border: 1px solid #e0e0e0;\n  border-radius: 6px;\n  padding: 10px;\n  cursor: pointer;\n  transition: 0.3s;\n}\n\n.batch-card:hover {\n  background-color: #f5f5f5;\n}\n\n/* 卡片选中状态 */\n.batch-card.selected {\n  background-color: #ffdddd; /* 选中后变红色或你想要的颜色 */\n  border-color: #f00;\n}\n\n/* 卡片底部信息 */\n.batch-card-info {\n  margin-top: 10px;\n  font-size: 14px;\n  color: #666;\n}\n\n/* 批量操作弹窗整体，可以复用已有的 .upload-container 样式，这里只是示例 */\n#batchModal {\n  display: none; /* 初始隐藏，与原有modal逻辑一致 */\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  background-color: rgba(0,0,0,0.5);\n  z-index: 2000; /* 设大一点以免被其他遮罩挡住 */\n}\n\n/* 弹窗内容区域 */\n.batch-content {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  background-color: white;\n  padding: 20px;\n  border-radius: 10px;\n  width: 80%;\n  max-width: 900px;\n  max-height: 80%;\n  overflow: hidden; /* 超出部分滚动 */\n  display: flex;\n  flex-direction: column;\n}\n\n/* 弹窗头部 */\n.batch-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  margin-bottom: 10px;\n}\n\n.batch-header h2 {\n  margin: 0;\n}\n\n.batch-controls {\n  display: flex;\n  gap: 10px;\n  margin-bottom: 10px;\n}\n\n/* 卡片区域 */\n.batch-grid {\n  flex: 1; /* 占满剩余可用空间 */\n  overflow-y: auto;\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n  gap: 10px;\n}\n\n/* 卡片本身 */\n.batch-card {\n  background-color: #fff;\n  border: 1px solid #e0e0e0;\n  border-radius: 6px;\n  padding: 10px;\n  cursor: pointer;\n  transition: 0.3s;\n}\n\n.batch-card:hover {\n  background-color: #f5f5f5;\n}\n\n/* 卡片选中状态 */\n.batch-card.selected {\n  background-color: #ffdddd; /* 选中后变红色或你想要的颜色 */\n  border-color: #f00;\n}\n\n/* 卡片底部信息 */\n.batch-card-info {\n  margin-top: 10px;\n  font-size: 14px;\n  color: #666;\n}\n.language-switch {\n  display: inline-block;\n  position: relative;\n}\n\n.nice-language-select {\n    display: inline-block;\n    padding: 6px 10px;\n    border: 1px solid #e0e0e0;\n    border-radius: 4px;\n    background-color: white; /* 白色背景 */\n    color: #666; /* 灰色字体 */\n    cursor: pointer;\n    font-size: 14px;\n    outline: none; /* 移除默认的黑色边框 */\n}\n\n.nice-language-select:hover {\n    background-color: #f5f5f5; /* 悬停时背景颜色稍微变灰 */\n\n}\n.info-box {\n    background: #fff;\n    padding: 20px;\n    border-radius: 8px;\n    border: 1px solid #e0e0e0;\n}\n\n.info-box p,\n.info-box li {\n    color:  #666;\n    line-height: 1.6;\n}\n\n.info-box ul {\n    padding-left: 20px;\n    margin-top: 10px;\n}\n\n.info-box li {\n    margin-bottom: 8px;\n}\n\n.info-box a {\n    color: #007bff;\n    text-decoration: none;\n}\n\n.info-box a:hover {\n    text-decoration: underline;\n}"
  },
  {
    "path": "static/setup.css",
    "content": "\n        select.t-input {\n    width: 300px;\n    padding: 8px;\n    border: 1px solid #ccc;\n    border-radius: 4px;\n    background-color: white;\n    transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;\n}\n\nselect.t-input:focus {\n    outline: none;\n    border-color: #007bff;\n    box-shadow: 0 0 5px rgba(0, 123, 255, 0.25);\n}\n\n        /* 容器与标题等基础样式 */\n        .t-container {\n            display: none;\n           max-width: 90%;\n\n            margin: 0 auto;\n            font-family: Arial, sans-serif;\n        }\n        .t-header-container {\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            margin-bottom: 20px;\n        }\n        .t-section {\n            border: 1px solid #ddd;\n            margin: 10px 0;\n            padding: 10px;\n            border-radius: 4px;\n            background: #fff;\n        }\n\n        /* 展开/折叠区域的基础样式 */\n        .t-section-header {\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            cursor: pointer;\n\n        }\n\n        /* 展开/折叠按钮：这里使用了一个“加号”图标，有旋转和淡入效果 */\n        .t-toggle-btn {\n            background: none;\n            border: none;\n            font-size: 18px;\n            cursor: pointer;\n            transition: transform 0.4s ease;\n            width: 30px;\n            height: 30px;\n            border-radius: 4px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n             color: #000;\n            position: relative;\n        }\n        .t-toggle-btn:hover {\n            background: #f0f0f0;\n        }\n        .t-toggle-btn.t-active {\n            transform: rotate(45deg);\n            color: #007bff;\n        }\n\n        /* 用于内容区域的动画展开：max-height + 透明度平滑过渡 */\n        .t-content {\n            max-height: 0;\n            overflow: hidden;\n            transition: max-height 0.4s ease, opacity 0.4s ease;\n            opacity: 0;\n        }\n        .t-content.t-active {\n            max-height: 1000px; /* 根据内容高度可适度加大 */\n            opacity: 1;\n        }\n\n        /* 子区域卡片 */\n        .t-sub-section {\n            margin-left: 20px;\n            padding: 10px;\n            border: 2px solid #eee;\n            margin-top: 10px;\n            border-radius: 4px;\n            background-color: white;\n        }\n\n        /* 输入框分组与标签 */\n        .t-input-group {\n            margin: 10px 0;\n        }\n        .t-input-group label {\n            display: inline-block;\n            width: 150px;\n            font-weight: bold;\n            color: #555;\n        }\n\n        /* 数量显示 */\n        .t-count-display {\n            padding: 5px 10px;\n            background-color: #f8f9fa;\n            border: 1px solid #ddd;\n            border-radius: 4px;\n            display: inline-block;\n        }\n\n        /* 美化输入框 */\n        .t-input {\n            width: 300px;\n            padding: 8px;\n            border: 1px solid #ccc;\n            border-radius: 4px;\n            transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;\n        }\n        .t-input:focus {\n            outline: none;\n            border-color: #007bff;\n            box-shadow: 0 0 5px rgba(0, 123, 255, 0.25);\n        }\n\n        /* 保存按钮 */\n        .t-save-btn {\n            background-color: #6366f1;\n            color: white;\n            border: none;\n            padding: 10px 20px;\n            border-radius: 5px;\n            cursor: pointer;\n            transition: background-color 0.3s;\n        }\n\n        .t-save-btn:hover {\n            background-color: #4f46e5;\n        }\n\n        .t-save-btn.success {\n            background-color: #22c55e;\n        }\n\n        .t-ppc {\n  /* 基础排版 */\n  width: 120px;\n  padding: 8px;\n  font-size: 14px;\n\n  /* 边框与圆角 */\n  border: 1px solid #ccc;\n  border-radius: 4px;\n\n  /* 其他外观 */\n  color: #333;\n  background-color: #f9f9f9;\n  outline: none;\n\n  /* 过渡，提升交互体验 */\n  transition: border-color 0.3s, box-shadow 0.3s;\n}\n\n.t-ppc:focus {\n  /* 获取焦点时边框颜色改变，比如蓝色 */\n  border-color: #4A90E2;\n  box-shadow: 0 0 5px rgba(74,144,226,0.5);\n}\n"
  },
  {
    "path": "static/setup.js",
    "content": "// 页面加载时获取配置\r\n    fetch('/config_json')\r\n        .then(response => response.json())\r\n        .then(data => {\r\n            initializeUI(data);\r\n        });\r\n\r\n    // 初始化UI\r\n  // 初始化UI\r\n    function initializeUI(data) {\r\n    document.getElementById('t-count').textContent = data.count;\r\n    document.getElementById('t-count').value = data.count;\r\n        document.getElementById('t-ppc').textContent = data.PPC;\r\n    document.getElementById('t-ppc').value = data.PPC;\r\ndocument.getElementById('count_article').textContent += data.count;\r\n\r\n\r\n    console.log('count', data.count);\r\n\r\n    // 初始化翻译服务 (这部分代码保持不变)\r\n    const translationServices = document.getElementById('t-translation-services');\r\n    Object.entries(data.translation_services).forEach(([service, config]) => {\r\n        const serviceDiv = createServiceSection(service, config);\r\n        translationServices.appendChild(serviceDiv);\r\n    });\r\n\r\n    // 初始化OCR服务 (这部分代码保持不变)\r\n    const ocrServices = document.getElementById('t-ocr-services');\r\n    Object.entries(data.ocr_services).forEach(([service, config]) => {\r\n        const serviceDiv = createServiceSection(service, config);\r\n        ocrServices.appendChild(serviceDiv);\r\n    });\r\n\r\n    // 初始化默认配置\r\n    const defaultServices = document.getElementById('t-default-services');\r\n    console.log('api',data.default_services.Translation_api)\r\n    const defaultConfig = {\r\n        'ocr_model': {\r\n            type: 'select',\r\n            options: ['true', 'false'],\r\n            value: data.default_services.ocr_model\r\n        },\r\n        'Enable_translation': {\r\n            type: 'select',\r\n            options: ['true', 'false'],\r\n            value: data.default_services.Enable_translation\r\n        },\r\n        'Translation_api': {\r\n            type: 'select',\r\n            options: ['AI302', 'Doubao', 'Qwen', 'deepseek', 'openai', 'deepl', 'youdao','Grok', 'ThirdParty', 'GLM', 'bing'],\r\n            value: data.default_services.Translation_api\r\n        },\r\n        'translation_prompt': {\r\n            type: 'textarea',\r\n            value: data.translation_prompt ? data.translation_prompt.system_prompt : 'You are a professional translator. Translate from {original_lang} to {target_lang}. Return only the translation without explanations or notes.'\r\n        }\r\n    };\r\n\r\n    // 在 initializeUI 函数中修改相关部分\r\nObject.entries(defaultConfig).forEach(([key, config]) => {\r\n    const inputGroup = document.createElement('div');\r\n    inputGroup.className = 't-input-group';\r\n\r\n    if (config.type === 'textarea') {\r\n        // 处理textarea类型（翻译提示词）\r\n        const textarea = document.createElement('textarea');\r\n        textarea.className = 't-input';\r\n        textarea.value = config.value;\r\n        textarea.rows = 3;\r\n        textarea.style.resize = 'vertical';\r\n        textarea.style.width = '100%';\r\n        textarea.placeholder = '请输入翻译提示词，可使用 {original_lang} 和 {target_lang} 作为占位符';\r\n        \r\n        if (key === 'translation_prompt') {\r\n            inputGroup.innerHTML = `<label data-lang-key=\"52\" style=\"font-weight: bold;\">翻译提示词:</label>`;\r\n        } else {\r\n            inputGroup.innerHTML = `<label>${key}:</label>`;\r\n        }\r\n        \r\n        inputGroup.appendChild(textarea);\r\n    } else {\r\n        // 处理select类型\r\n        const select = document.createElement('select');\r\n        select.className = 't-input';\r\n\r\n        config.options.forEach(option => {\r\n            const optionElement = document.createElement('option');\r\n            optionElement.value = option;\r\n            optionElement.textContent = option;\r\n\r\n            // 修改选项匹配逻辑\r\n            if (key === 'Translation_api') {\r\n                // 直接比较字符串值\r\n                optionElement.selected = (option === config.value);\r\n                console.log(`Translation API option: ${option}, config value: ${config.value}, selected: ${optionElement.selected}`);\r\n            }  else if (key === 'ocr_model' || key === 'Enable_translation' ) {\r\n                    const optionBool = option.toLowerCase() === 'true';\r\n                    optionElement.selected = (optionBool === config.value);\r\n                }\r\n\r\n            select.appendChild(optionElement);\r\n        });\r\n        \r\n        if (key === 'Enable_translation') {\r\n            inputGroup.innerHTML = `<label style=\"font-size: 80%; font-weight: bold;\">${key}:</label>`;\r\n        } else {\r\n            inputGroup.innerHTML = `<label>${key}:</label>`;\r\n        }\r\n\r\n        inputGroup.appendChild(select);\r\n    }\r\n    \r\n    defaultServices.appendChild(inputGroup);\r\n});\r\n\r\n}\r\n\r\n    // 创建服务配置区域\r\n    function createServiceSection(serviceName, config) {\r\n        const section = document.createElement('div');\r\n        section.className = 't-sub-section';\r\n\r\n        const header = document.createElement('div');\r\n        header.className = 't-section-header';\r\n        header.innerHTML = `\r\n            <h4>${serviceName}</h4>\r\n            <button class=\"t-toggle-btn\">+</button>\r\n        `;\r\n\r\n        const content = document.createElement('div');\r\n        content.className = 't-content';\r\n\r\n        Object.entries(config).forEach(([key, value]) => {\r\n            const inputGroup = document.createElement('div');\r\n            inputGroup.className = 't-input-group';\r\n            inputGroup.innerHTML = `\r\n                <label>${key}:</label>\r\n                <input type=\"text\" class=\"t-input\" value=\"${value}\">\r\n            `;\r\n            content.appendChild(inputGroup);\r\n        });\r\n\r\n\r\n        section.appendChild(header);\r\n        section.appendChild(content);\r\n\r\n        return section;\r\n    }\r\n\r\n    // 添加展开/折叠功能\r\n    document.addEventListener('click', function(e) {\r\n        if (e.target.classList.contains('t-toggle-btn')) {\r\n            const button = e.target;\r\n            const content = button.closest('.t-section-header').nextElementSibling;\r\n            button.classList.toggle('t-active');\r\n            content.classList.toggle('t-active');\r\n        }\r\n    });\r\n\r\n    // 添加自动保存功能\r\n    let saveTimeout;\r\n    document.addEventListener('input', function(e) {\r\n        if (e.target.classList.contains('t-input') || e.target.tagName === 'TEXTAREA') {\r\n            clearTimeout(saveTimeout);\r\n            saveTimeout = setTimeout(() => {\r\n                // 收集当前所有配置数据\r\n                const config = collectConfig();\r\n                // 发送到后端\r\n                fetch('/update_config', {\r\n                    method: 'POST',\r\n                    headers: {\r\n                        'Content-Type': 'application/json',\r\n                    },\r\n                    body: JSON.stringify(config)\r\n                });\r\n            }, 5000);\r\n        }\r\n    });\r\nasync function saveall() {\r\n    const saveall = document.getElementById('saveall');\r\n\r\n\r\n// 添加切换事件监听\r\n\r\n\r\n    try {\r\n        // 发送数据到后端\r\n\r\n        const config = collectConfig();\r\n\r\n        const response = await fetch('/save_all', {\r\n            method: 'POST',\r\n            headers: {\r\n                'Content-Type': 'application/json',\r\n            },\r\n             body: JSON.stringify(config)\r\n        });\r\n\r\n        if (!response.ok) {\r\n            throw new Error('保存失败');\r\n        }\r\n\r\n        // 显示成功状态\r\n        saveall.innerHTML = '✓';\r\n        saveall.classList.add('success');\r\n\r\n        // 2秒后恢复按钮状态\r\n        setTimeout(() => {\r\n            saveall.innerHTML = '保存所有修改';\r\n            saveall.classList.remove('success');\r\n        }, 2000);\r\n\r\n\r\n\r\n    } catch (error) {\r\n        console.error('保存设置失败:', error);\r\n        alert('保存设置失败，请重试');\r\n    }\r\n}\r\n    // 保存所有修改\r\n    document.querySelector('.t-save-btn').addEventListener('click', function() {\r\n        const config = collectConfig();\r\n        fetch('/save_all', {\r\n            method: 'POST',\r\n            headers: {\r\n                'Content-Type': 'application/json',\r\n            },\r\n            body: JSON.stringify(config)\r\n        });\r\n    });\r\n\r\n    // 收集所有配置数据\r\n    // 收集所有配置数据\r\nfunction collectConfig() {\r\n    const config = {\r\n      count: document.getElementById('t-count').value,\r\n        PPC: parseInt(document.getElementById('t-ppc').value, 10),\r\n        translation_services: {},\r\n        ocr_services: {},\r\n        default_services: {}\r\n    };\r\n\r\n    // 收集翻译服务配置\r\n    const translationServices = document.getElementById('t-translation-services');\r\n    [...translationServices.getElementsByClassName('t-sub-section')].forEach(section => {\r\n        const serviceName = section.querySelector('h4').textContent;\r\n        config.translation_services[serviceName] = {};\r\n        [...section.getElementsByClassName('t-input-group')].forEach(group => {\r\n            const key = group.querySelector('label').textContent.replace(':', '');\r\n            const value = group.querySelector('input').value;\r\n            config.translation_services[serviceName][key] = value;\r\n        });\r\n    });\r\n\r\n    // 收集OCR服务配置\r\n    const ocrServices = document.getElementById('t-ocr-services');\r\n    [...ocrServices.getElementsByClassName('t-sub-section')].forEach(section => {\r\n        const serviceName = section.querySelector('h4').textContent;\r\n        config.ocr_services[serviceName] = {};\r\n        [...section.getElementsByClassName('t-input-group')].forEach(group => {\r\n            const key = group.querySelector('label').textContent.replace(':', '');\r\n            const value = group.querySelector('input').value;\r\n            config.ocr_services[serviceName][key] = value;\r\n        });\r\n    });\r\n\r\n    // 收集默认配置\r\n// 收集默认配置\r\n        const defaultServices = document.getElementById('t-default-services');\r\n        [...defaultServices.getElementsByClassName('t-input-group')].forEach(group => {\r\n            const key = group.querySelector('label').textContent.replace(':', '');\r\n            let value;\r\n            \r\n            // 检查是否为textarea\r\n            const textarea = group.querySelector('textarea');\r\n            const select = group.querySelector('select');\r\n            \r\n            if (textarea) {\r\n                value = textarea.value;\r\n                if (key === '翻译提示词') {\r\n                    // 翻译提示词需要单独处理\r\n                    if (!config.translation_prompt) {\r\n                        config.translation_prompt = {};\r\n                    }\r\n                    config.translation_prompt.system_prompt = value;\r\n                    return; // 跳过后续处理\r\n                }\r\n            } else if (select) {\r\n                value = select.value;\r\n            }\r\n\r\n            // 对特定key进行布尔值转换\r\n            if(key === 'ocr_model' || key === 'Enable_translation' ) {\r\n                value = value === 'true' ? true : false;\r\n            }\r\n\r\n            config.default_services[key] = value;\r\n        });\r\n\r\n\r\n    return config;\r\n}\r\n\r\n// 在加载翻译服务配置时，确保处理Grok选项\r\nfunction loadTranslationServices(config) {\r\n    const container = document.getElementById('t-translation-services');\r\n    // ...existing code...\r\n\r\n    // 确保在创建服务配置UI时包含Grok\r\n    // 使用正确的键名'Grok'而不是'grok'\r\n    if (config.translation_services && config.translation_services.Grok) {\r\n        const grokDiv = document.createElement('div');\r\n        grokDiv.className = 't-service';\r\n        grokDiv.innerHTML = `\r\n            <h4 data-lang-key=\"48\">Grok Translate API</h4>\r\n            <div class=\"t-input-group\">\r\n                <label>Auth Key:</label>\r\n                <input type=\"password\" name=\"grok_auth_key\" value=\"${config.translation_services.Grok.auth_key || ''}\">\r\n            </div>\r\n            <div class=\"t-input-group\">\r\n                <label>Model Name:</label>\r\n                <input type=\"text\" name=\"grok_model_name\" value=\"${config.translation_services.Grok.model_name || 'grok-2-latest'}\">\r\n            </div>\r\n        `;\r\n        container.appendChild(grokDiv);\r\n    }\r\n    \r\n    // 确保在创建服务配置UI时包含GLM\r\n    if (config.translation_services && config.translation_services.GLM) {\r\n        const glmDiv = document.createElement('div');\r\n        glmDiv.className = 't-service';\r\n        glmDiv.innerHTML = `\r\n            <h4 data-lang-key=\"50\">GLM Translate API</h4>\r\n            <div class=\"t-input-group\">\r\n                <label>Auth Key:</label>\r\n                <input type=\"password\" name=\"glm_auth_key\" value=\"${config.translation_services.GLM.auth_key || ''}\">\r\n            </div>\r\n            <div class=\"t-input-group\">\r\n                <label>Model Name:</label>\r\n                <input type=\"text\" name=\"glm_model_name\" value=\"${config.translation_services.GLM.model_name || 'glm-4-plus'}\">\r\n            </div>\r\n        `;\r\n        container.appendChild(glmDiv);\r\n    }\r\n    \r\n    // 添加ThirdParty服务配置\r\n    if (config.translation_services && config.translation_services.ThirdParty) {\r\n        const thirdPartyDiv = document.createElement('div');\r\n        thirdPartyDiv.className = 't-sub-section';\r\n        thirdPartyDiv.innerHTML = `\r\n            <div class=\"t-section-header\">\r\n                <h4 data-lang-key=\"49\">ThirdParty</h4>\r\n                <button class=\"t-toggle-btn\">+</button>\r\n            </div>\r\n            <div class=\"t-content\">\r\n                <div class=\"t-input-group\">\r\n                    <label>api_url:</label>\r\n                    <input type=\"text\" class=\"t-input\" value=\"${config.translation_services.ThirdParty.api_url || 'https://api.example.com/v1/chat/completions'}\">\r\n                </div>\r\n                <div class=\"t-input-group\">\r\n                    <label>auth_key:</label>\r\n                    <input type=\"password\" class=\"t-input\" value=\"${config.translation_services.ThirdParty.auth_key || ''}\">\r\n                </div>\r\n                <div class=\"t-input-group\">\r\n                    <label>model_name:</label>\r\n                    <input type=\"text\" class=\"t-input\" value=\"${config.translation_services.ThirdParty.model_name || 'custom-model'}\">\r\n                </div>\r\n            </div>\r\n        `;\r\n        container.appendChild(thirdPartyDiv);\r\n    } else {\r\n        // 如果ThirdParty配置不存在，则创建一个默认的\r\n        const thirdPartyDiv = document.createElement('div');\r\n        thirdPartyDiv.className = 't-sub-section';\r\n        thirdPartyDiv.innerHTML = `\r\n            <div class=\"t-section-header\">\r\n                <h4>ThirdParty</h4>\r\n                <button class=\"t-toggle-btn\">+</button>\r\n            </div>\r\n            <div class=\"t-content\">\r\n                <div class=\"t-input-group\">\r\n                    <label>api_url:</label>\r\n                    <input type=\"text\" class=\"t-input\" value=\"https://api.example.com/v1/chat/completions\">\r\n                </div>\r\n                <div class=\"t-input-group\">\r\n                    <label>auth_key:</label>\r\n                    <input type=\"password\" class=\"t-input\" value=\"\">\r\n                </div>\r\n                <div class=\"t-input-group\">\r\n                    <label>model_name:</label>\r\n                    <input type=\"text\" class=\"t-input\" value=\"custom-model\">\r\n                </div>\r\n            </div>\r\n        `;\r\n        container.appendChild(thirdPartyDiv);\r\n    }\r\n    \r\n    // 添加Bing服务配置UI\r\n    if (config.translation_services && config.translation_services.bing) {\r\n        const bingDiv = document.createElement('div');\r\n        bingDiv.className = 't-service';\r\n        bingDiv.innerHTML = `\r\n            <h4 data-lang-key=\"51\">Bing Translate API</h4>\r\n            <div class=\"t-input-group\">\r\n                <label>无需配置API密钥</label>\r\n            </div>\r\n        `;\r\n        container.appendChild(bingDiv);\r\n    } else {\r\n        // 如果Bing配置不存在，则创建一个默认的\r\n        const bingDiv = document.createElement('div');\r\n        bingDiv.className = 't-sub-section';\r\n        bingDiv.innerHTML = `\r\n            <div class=\"t-section-header\">\r\n                <h4 data-lang-key=\"51\">Bing</h4>\r\n                <button class=\"t-toggle-btn\">+</button>\r\n            </div>\r\n            <div class=\"t-content\">\r\n                <div class=\"t-input-group\">\r\n                    <label>无需API密钥，直接使用微软Bing翻译</label>\r\n                </div>\r\n            </div>\r\n        `;\r\n        container.appendChild(bingDiv);\r\n    }\r\n}\r\n\r\n"
  },
  {
    "path": "static/thumbnail/...txt",
    "content": ""
  },
  {
    "path": "update_recent.py",
    "content": "import os\r\nimport json\r\nimport datetime\r\nfrom typing import List, Dict, Any\r\nimport glob\r\nfrom collections import OrderedDict\r\nimport re\r\nimport shutil\r\n\r\ndef parse_merged_filename(filename: str) -> Dict[str, str]:\r\n    \"\"\"从合并PDF文件名解析出原始文件名、原始语言和目标语言\"\"\"\r\n    # 格式为：原始文件名_原始语言_目标语言.pdf\r\n    pattern = r\"(.+)_([\\w-]+)_([\\w-]+)\\.pdf$\"\r\n    match = re.match(pattern, filename)\r\n    \r\n    if match:\r\n        original_name = match.group(1) + \".pdf\"  # 添加.pdf后缀\r\n        original_lang = match.group(2)\r\n        target_lang = match.group(3)\r\n        return {\r\n            \"original_name\": original_name,\r\n            \"original_language\": original_lang,\r\n            \"target_language\": target_lang\r\n        }\r\n    else:\r\n        # 如果不符合格式，返回默认值并确保有.pdf后缀\r\n        name_without_ext = filename.rsplit(\".\", 1)[0]  # 去掉扩展名\r\n        return {\r\n            \"original_name\": name_without_ext + \".pdf\",\r\n            \"original_language\": \"auto\",\r\n            \"target_language\": \"zh\"\r\n        }\r\n\r\ndef get_file_info(file_path: str) -> Dict[str, Any]:\r\n    \"\"\"从文件路径获取文件信息\"\"\"\r\n    filename = os.path.basename(file_path)\r\n    creation_time = os.path.getctime(file_path)\r\n    date_str = datetime.datetime.fromtimestamp(creation_time).strftime('%Y-%m-%d %H:%M:%S')\r\n    \r\n    # 解析文件名\r\n    parsed_info = parse_merged_filename(filename)\r\n    \r\n    # 创建有序字典，确保属性按指定顺序排列\r\n    ordered_info = OrderedDict()\r\n    ordered_info[\"index\"] = 0  # 临时值，会在后面被更新\r\n    ordered_info[\"date\"] = date_str\r\n    ordered_info[\"name\"] = parsed_info[\"original_name\"]\r\n    ordered_info[\"original_language\"] = parsed_info[\"original_language\"]\r\n    ordered_info[\"target_language\"] = parsed_info[\"target_language\"]\r\n    ordered_info[\"read\"] = \"0\"  # 默认为未读\r\n    ordered_info[\"statue\"] = \"1\"  # 默认状态为1\r\n    \r\n    return ordered_info\r\n\r\ndef update_config_count(count: int) -> bool:\r\n    \"\"\"\r\n    更新config.json中的count值为指定的数量\r\n    \r\n    Args:\r\n        count: 要设置的count值\r\n        \r\n    Returns:\r\n        bool: 操作是否成功\r\n    \"\"\"\r\n    try:\r\n        # 读取config.json文件\r\n        config_path = \"config.json\"\r\n        if os.path.exists(config_path):\r\n            with open(config_path, \"r\", encoding=\"utf-8\") as f:\r\n                config = json.load(f)\r\n            \r\n            # 更新count值\r\n            config[\"count\"] = count\r\n            \r\n            # 写回文件\r\n            with open(config_path, \"w\", encoding=\"utf-8\") as f:\r\n                json.dump(config, f, ensure_ascii=False, indent=2)\r\n            \r\n            print(f\"已更新config.json的count值为: {count}\")\r\n            return True\r\n        else:\r\n            print(f\"错误: 找不到config.json文件\")\r\n            return False\r\n    except Exception as e:\r\n        print(f\"更新config.json的count值时发生错误: {str(e)}\")\r\n        return False\r\n\r\ndef validate_json_file(file_path: str) -> bool:\r\n    \"\"\"\r\n    验证JSON文件格式是否正确\r\n    \r\n    Args:\r\n        file_path: JSON文件路径\r\n        \r\n    Returns:\r\n        bool: 文件格式是否有效\r\n    \"\"\"\r\n    try:\r\n        if os.path.exists(file_path):\r\n            with open(file_path, \"r\", encoding=\"utf-8\") as f:\r\n                json.load(f)\r\n            return True\r\n        return False\r\n    except Exception as e:\r\n        print(f\"JSON文件格式无效: {str(e)}\")\r\n        return False\r\n\r\ndef update_recent_json():\r\n    \"\"\"更新recent.json文件，先清空现有配置，然后从索引0开始重新生成\"\"\"\r\n    # 从merged_pdf目录读取文件\r\n    merged_path = os.path.join(\"static\", \"merged_pdf\")\r\n    \r\n    # 创建备份\r\n    if os.path.exists(\"recent.json\"):\r\n        try:\r\n            shutil.copy2(\"recent.json\", \"recent.json.bak\")\r\n            print(f\"已创建备份文件: recent.json.bak\")\r\n        except Exception as e:\r\n            print(f\"创建备份文件失败: {str(e)}\")\r\n    \r\n    # 扫描merged_pdf目录获取文件\r\n    if not os.path.exists(merged_path):\r\n        print(f\"警告: 目录不存在 {merged_path}\")\r\n        try:\r\n            os.makedirs(merged_path, exist_ok=True)\r\n        except Exception as e:\r\n            print(f\"创建目录失败: {str(e)}\")\r\n    \r\n    merged_files = glob.glob(os.path.join(merged_path, \"*.pdf\"))\r\n    new_entries = []\r\n    \r\n    for file_path in merged_files:\r\n        file_info = get_file_info(file_path)\r\n        new_entries.append(file_info)\r\n    \r\n    # 从索引0开始分配\r\n    for i, entry in enumerate(new_entries):\r\n        entry[\"index\"] = i \r\n    \r\n    # 保存前先验证数据格式\r\n    try:\r\n        # 使用json.dumps检查序列化是否正常\r\n        json_str = json.dumps(new_entries, ensure_ascii=False, indent=2)\r\n        \r\n        # 写入文件\r\n        with open(\"recent.json\", \"w\", encoding=\"utf-8\") as f:\r\n            f.write(json_str)\r\n        \r\n        # 验证写入的文件\r\n        if not validate_json_file(\"recent.json\"):\r\n            raise Exception(\"写入的JSON文件验证失败\")\r\n        \r\n        # 更新config.json中的count值为新条目的数量\r\n        update_config_count(len(new_entries))\r\n        \r\n        print(f\"已重置并更新recent.json，共 {len(new_entries)} 条记录\")\r\n    except Exception as e:\r\n        print(f\"更新recent.json文件失败: {str(e)}\")\r\n        # 尝试恢复备份\r\n        if os.path.exists(\"recent.json.bak\"):\r\n            try:\r\n                shutil.copy2(\"recent.json.bak\", \"recent.json\")\r\n                print(\"已从备份恢复recent.json文件\")\r\n            except Exception as e2:\r\n                print(f\"从备份恢复失败: {str(e2)}\")\r\n\r\nif __name__ == \"__main__\":\r\n    update_recent_json()\r\n"
  }
]