<Project-6 pdf2tx> Python Flask 应用:图片PDF图书的中文翻译解决方案

发布于:2024-10-11 ⋅ 阅读:(60) ⋅ 点赞:(0)

重要更新!

Modified on 8oct24.   
P6已经被 P8 替代,后着支持多任务,多翻译机。在速度与资源占用上,都好于这个P6。 
新的 P8 文章链接:

 <Project-8 pdf2tx-MM> Python Flask应用:在浏览器中翻译PDF文件 NLTK OCR 多线程 指定翻译器 改进后的P6

目的

       能够从扫描的 PDF 图片中提取文本,并将其翻译成中文后生成对应的译文。

起因

看维基百科:https://en.wikipedia.org/wiki/Soviet%E2%80%93Afghan_War 苏联阿富汗战争。里面提到中国对阿富汗圣战者(当时反苏联,反傀儡阿富汗政府【阿富汗民主共和国】入侵的阿富汗民兵游机队)给予军事上的援助,当时对圣战者主要援助国:巴基斯坦、美国、英国、中国、伊朗和波斯湾阿拉伯国家。想得到更多的内容,就看到一本书:《Xinjiang: China's Muslim Borderland》新疆:中国的穆斯林边疆 ,这书$18,然后... 每页都是图片,还是英文的,一个字也看不懂。只能让电脑去翻译,还能拼出这个文章。

下面有些是复制Chatgpt回答内容:根据代码内容,写一篇这个程序的介绍,项目名称 pdf2tx,但是能用的不多,看到还要提高prompt表述能力。

pdf2tx 是一个旨在将图片格式的 PDF 图书自动翻译成中文的项目,尤其适合需要将外文书籍或文档快速翻译为中文的用户。该工具结合了 OCR(光学字符识别)技术与机器翻译,能够从扫描的 PDF 图片中提取文本,并将其翻译成中文后生成对应的译文。

功能

  • 上传 PDF 文件:用户通过网页界面上传 PDF 文件,上传页面(upload.html)提供了简单的表单,支持 .pdf 文件的选择和提交​(upload)。

  • 进度显示:上传后,应用程序会进入处理状态,并通过进度条(processing.html)显示处理进度。使用了 Socket.IO 来实时更新处理进度,增强用户体验​(processing)。

  • 文本提取与翻译:程序使用 pdf2image 将 PDF 转换为图像,然后利用 pytesseract 进行 OCR(光学字符识别)以提取文字内容。随后,通过 deep_translator 库将提取的文字翻译为目标语言​(requirements)。

  • 翻译结果展示:处理完成后,结果页面(result.html)展示原文和译文。文本以双栏布局展示,方便对比查看​(result)。

  1. 上传 PDF 文件: 用户需要通过网页界面上传需要翻译的 PDF 文件。可以访问 upload.html 页面,选择 PDF 文件并点击上传按钮。

  2. 翻译过程: 文件上传后,系统会开始处理,显示处理进度。用户可以通过网页界面查看 OCR 与翻译的实时进展。

  3. 查看翻译结果: 翻译完成后,用户可以查看原文与译文的对比结果。网页会将翻译后的内容展示在左右两个部分,方便用户对照查看。

只下存了5页用于测试程序。

程序的逻辑

PDF转为图像
   -> OCR提取图片中的文字
           -> 文字交给翻译器
                       -> 翻译的结果在网页上显示

因为使用时,可能会用到未授权问题,上传代码没有保存内容的功能。

代码内容介绍

pdf2tx 的代码结构由以下几个关键部分组成:

  1. 主应用程序 (app.py)app.py 是整个项目的核心,用于处理用户请求、管理前端页面以及实现 OCR 和翻译功能。该程序基于 Flask 框架,提供了简单的 Web 服务,同时使用 flask_socketio 实现前端与后端之间的实时通信

  2. OCR 与翻译功能: OCR 和翻译功能是 pdf2tx 的核心逻辑。它们通过 pdf2image 将 PDF 页面转换为图像,然后使用 pytesseract 提取文本,再通过 deep_translator 将提取的文本翻译为中文。

  3. 前端 HTML 模板

    • upload.html:用户可以上传 PDF 文件进行翻译。

    • processing.html:显示文件处理进度的页面,包含一个进度条,实时显示 OCR 和翻译的进度。

    • result.html:展示原文和译文的页面,用户可以对比查看翻译结果。

    前端页面与后端通过 SocketIO 实现实时交互,确保用户可以直观地看到文件处理进度和最终的翻译结果。

完整代码

目录结构

pdf2tx/
├── app.py                    # 主程序文件,包含应用程序的逻辑
├── Dockerfile                # Docker 配置文件,用于容器化部署
├── requirements.txt          # Python 依赖文件,列出所需安装的包
├── templates/                # HTML 模板文件夹
        ├─ upload.html           # 上传页面
        ├── processing.html       # 处理中页面
        └── result.html           # 结果展示页面

目录结构说明

  1. app.py:包含应用程序的核心逻辑,例如路由定义、文件处理、文字提取、翻译等。
  2. Dockerfile:用于定义项目的 Docker 镜像,方便部署和环境管理。
  3. requirements.txt:列出了项目所需的 Python 库,如 Flask、pdf2imagepytesseract 等​(requirements)。
  4. templates/:包含应用程序的 HTML 模板文件,用于与用户交互的网页。
    • upload.html:用于文件上传的页面​(upload)。
    • processing.html:用于显示文件处理进度的页面​(processing)。
    • result.html:用于展示翻译结果的页面​(result)。

代码

app.py

import eventlet
eventlet.monkey_patch()

import os
import uuid
from flask import Flask, render_template, request, redirect, url_for
from flask_socketio import SocketIO
from pdf2image import convert_from_path
import pytesseract
from deep_translator import GoogleTranslator
from werkzeug.utils import secure_filename
import logging

# 初始化日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = Flask(__name__)
app.config['ALLOWED_EXTENSIONS'] = {'pdf'}

socketio = SocketIO(app, cors_allowed_origins="*")

# 获取 app.py 文件所在的目录
current_directory = os.path.dirname(os.path.abspath(__file__))

# 设置 uploads_FOLDER 为 'uploads' 目录的绝对路径
uploads_directory = os.path.join(current_directory, 'uploads')
app.config['UPLOAD_FOLDER'] = uploads_directory
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024  # 50MB

# 切换当前工作目录到项目目录
os.makedirs(uploads_directory, exist_ok=True)

# 创建上传目录(如果不存在)
if not os.path.exists(app.config['UPLOAD_FOLDER']):
    os.makedirs(app.config['UPLOAD_FOLDER'])

# 允许的文件扩展名检查函数
def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']

# 翻译函数
def translate_text(text):
    max_length = 5000  # Google Translate 单次请求的最大字符数
    translated_text = ''
    paragraphs = text.split('\n')
    temp_text = ''
    for para in paragraphs:
        if len(temp_text) + len(para) < max_length:
            temp_text += para + '\n'
        else:
            try:
                translated = GoogleTranslator(source='auto', target='zh-CN').translate(text=temp_text)
            except Exception as e:
                print(f"翻译失败:{e}")
                translated = temp_text  # 如果翻译失败,使用原文本
            translated_text += translated + '\n'
            temp_text = para + '\n'
    if temp_text:
        try:
            translated = GoogleTranslator(source='auto', target='zh-CN').translate(text=temp_text)
        except Exception as e:
            print(f"翻译失败:{e}")
            translated = temp_text  # 如果翻译失败,使用原文本
        translated_text += translated + '\n'
    return translated_text
#注:可以尝试使用DeepL, 百度 有免费期,或chatgpt gemini 就是比较贵

# PDF 转换为图像函数
def pdf_to_images(filepath):
    try:
        images = convert_from_path(filepath)
    except Exception as e:
        logger.error(f"PDF 转换失败:{e}")
        raise e
    return images

@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        # 检查文件是否在请求中
        if 'file' not in request.files:
            return '请求中没有文件部分'
        file = request.files['file']
        if file.filename == '':
            return '未选择文件'
        if file and allowed_file(file.filename):
            # 使用唯一且安全的文件名使用UUID
            ext = os.path.splitext(file.filename)[1]
            filename = f"{uuid.uuid4().hex}{ext}"
            filename = secure_filename(filename)
            filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
            try:
                file.save(filepath)
            except Exception as e:
                return f"文件保存失败:{e}"
            # 重定向到处理页面,文件名作为查询参数
            return redirect(url_for('processing', filename=filename))
    return render_template('upload.html')

@app.route('/processing')
def processing():
    filename = request.args.get('filename', None)
    if not filename:
        return '文件未找到'
    return render_template('processing.html', filename=filename)

@app.route('/result')
def result():
    return render_template('result.html')

@socketio.on('connect')
def connect():
    logger.info('客户端已连接')

@socketio.on('start_processing')
def start_processing(data):
    sid = request.sid
    socketio.start_background_task(process_file, data, sid)

def process_file(data, sid):
    filename = data.get('filename', None)
    if not filename:
        socketio.emit('error', {'data': '文件未找到'}, room=sid)
        return
    filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
    if not os.path.exists(filepath):
        socketio.emit('error', {'data': '文件未找到'}, room=sid)
        return
    socketio.emit('progress', {'data': 10}, room=sid)
    try:
        images = pdf_to_images(filepath)
    except Exception as e:
        socketio.emit('error', {'data': f'PDF 转换失败:{e}'}, room=sid)
        return
    socketio.emit('progress', {'data': 30}, room=sid)
    extracted_text = ''
    total_pages = len(images)
    for i, image in enumerate(images):
        try:
            text = pytesseract.image_to_string(image)
        except Exception as e:
            socketio.emit('error', {'data': f'OCR 识别失败:{e}'}, room=sid)
            return
        extracted_text += text + '\n'
        progress = 30 + int(50 * (i + 1) / total_pages)
        socketio.emit('progress', {'data': progress}, room=sid)
    try:
        translated_text = translate_text(extracted_text)
    except Exception as e:
        socketio.emit('error', {'data': f'翻译失败:{e}'}, room=sid)
        return
    socketio.emit('progress', {'data': 100}, room=sid)
    socketio.emit('result', {'original': extracted_text, 'translated': translated_text}, room=sid)
    try:
        os.remove(filepath)
    except Exception as e:
        logger.error(f"删除文件失败:{e}")

if __name__ == '__main__':
    socketio.run(app, host='0.0.0.0', port=9004, debug=True)

upload.html

<!-- templates/upload.html -->

<!doctype html>
<html>
<head>
    <title>上传 PDF 文件</title>
</head>
<body>
    <h1>上传 PDF 文件以进行翻译</h1>
    <form method="post" enctype="multipart/form-data">
        <input type="file" name="file" accept=".pdf" required>
        <input type="submit" value="上传并开始处理">
    </form>
</body>
</html>

processing.html

<!doctype html>
<html>
<head>
    <title>处理中...</title>
    <script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
    <style>
        #progress-bar {
            width: 50%;
            background-color: #f3f3f3;
            margin: 20px 0;
        }
        #progress-bar-fill {
            height: 30px;
            width: 0%;
            background-color: #4caf50;
            text-align: center;
            line-height: 30px;
            color: white;
        }
    </style>
</head>
<body>
    <h1>文件正在处理中,请稍候...</h1>
    <div id="progress-bar">
        <div id="progress-bar-fill">0%</div>
    </div>
    <script>
        var filename = "{{ filename }}";

        var socket = io();

        socket.on('connect', function() {
            socket.emit('start_processing', {'filename': filename});
        });

        socket.on('progress', function(msg) {
            var progress = msg.data;
            var progressBarFill = document.getElementById('progress-bar-fill');
            progressBarFill.style.width = progress + '%';
            progressBarFill.innerText = progress + '%';
        });

        socket.on('result', function(msg) {
            // 将结果保存到 LocalStorage
            localStorage.setItem('original', msg.original);
            localStorage.setItem('translated', msg.translated);
            // 跳转到结果页面
            window.location.href = '/result';
        });

        socket.on('error', function(msg) {
            alert('错误:' + msg.data);
            window.location.href = '/';
        });
    </script>
</body>
</html>

result.html

<!-- templates/result.html -->

<!doctype html>
<html>
<head>
    <title>翻译结果</title>
    <style>
        .container {
            display: flex;
        }
        .content {
            width: 50%;
            padding: 20px;
            box-sizing: border-box;
            overflow-y: scroll;
            height: 90vh;
        }
        .original {
            background-color: #f9f9f9;
        }
        .translated {
            background-color: #eef9f1;
        }
        pre {
            white-space: pre-wrap;
            word-wrap: break-word;
        }
    </style>
</head>
<body>
    <h1>翻译结果</h1>
    <div class="container">
        <div class="content original">
            <h2>原文</h2>
            <pre id="original-text"></pre>
        </div>
        <div class="content translated">
            <h2>译文</h2>
            <pre id="translated-text"></pre>
        </div>
    </div>
    <script>
        // 从 LocalStorage 获取结果
        document.getElementById('original-text').innerText = localStorage.getItem('original');
        document.getElementById('translated-text').innerText = localStorage.getItem('translated');
        // 清除 LocalStorage
        localStorage.removeItem('original');
        localStorage.removeItem('translated');
    </script>
</body>
</html>

Dockerfile

# 使用官方的 Python 3.9 slim 版作为基础镜像
FROM python:3.12.3-slim

# 安装必要的系统依赖
RUN apt-get update && apt-get install -y \
    tesseract-ocr \
    poppler-utils \
    libtesseract-dev \
    libleptonica-dev \
    pkg-config \
    && rm -rf /var/lib/apt/lists/*

# 设置工作目录
WORKDIR /app

# 复制当前目录内容到工作目录
COPY . /app

# 安装 Python 依赖
RUN pip install --upgrade pip

RUN pip install --no-cache-dir -r requirements.txt

# 暴露应用运行的端口
EXPOSE 9004

# 设置环境变量,防止 Python 输出被缓冲
ENV PYTHONUNBUFFERED=1

# 运行应用
CMD ["python", "app.py"]

requirements.txt

Flask
flask_socketio
pdf2image
pytesseract
deep_translator
Werkzeug
eventlet

Windows 运行

  1. 安装依赖环境: 在 Windows 系统上,首先需要安装 Python 以及所需的库。主要依赖库见 requirements.txt:

  2. 运行项目: cmd: python app.py 脚本来启动项目,用户可以通过本地浏览器访问应用,上传需要翻译的 PDF 文件。

  3. OCR tesseract 下载: Home · UB-Mannheim/tesseract Wiki · GitHub

  4. 因为PDF2IMAGE 赖于 Poppler 来将 PDF 转换为图像, 需要下载windows 的二进制程序:
    https://github.com/oschwartz10612/poppler-windows 并且要加入到系统的路径中。如果你同我一样没有重启 Visual Studio Code,它不能加入PATH更新的内容。

怎么加path,见下图:

        5. 重启cmd在项目目录下,运行 python app.py

Docker 部署

如果用户希望更方便地在服务器或其他平台上部署该工具,可以选择使用 Docker。下面是 Docker 的部署步骤:

必要文件

Dockerfile, requirements.txt, 程序4文件, 如果是Linux不需要单独手动安装OCR与Poppler

构建 Docker 镜像:在Dockerfile目录下

docker build -t pdf2tx . #创建一个image

docker run -d -p 9004:9004 --name pdf2tx-containter pdf2tx       # 指定运行端口号是9004, 命名容器的名字 pdf2tx-container, 来自镜像 pdf2tx

运行容器

docker start pdf2tx-container

这样,用户可以通过访问 http://NAS_IP:9004 来使用翻译功能。

总结

pdf2tx 提供了一个便捷的方案,用于将图片 PDF 文件中的内容翻译为中文,无需手动提取和处理图片文本。用户只需上传 PDF 文件,工具会自动完成文字识别与翻译,并最终以对照的形式展示翻译结果。无论是在 Windows 系统中直接运行,还是通过 Docker 部署,pdf2tx 都能为用户提供高效的翻译体验。

另,试着在NAS上添加swapfile内存

如果在家用QNAP NAS上使用这个程序,如果PDF文件比较大,可能会造成内存不够,使NAS 宕机。

我的TS-453D 使用top命令,看到:
Mem: 3671064K used, 185496K free, 34592K shrd, 6844K buff, 409892K cached
试了一本400页的,NAS就崩溃了   (物理4GB内存+16GB swap)

我记得以前的UNIX/linux还有个swapfile做扩展内存,它已经进化的没有了吗?

在NAS终端里,运行命令:

[/share/CACHEDEV1_DATA] # df -h
Filesystem                Size      Used Available Use% Mounted on
none                    400.0M    347.4M     52.6M  87% /
devtmpfs                  1.8G      4.0K      1.8G   0% /dev
tmpfs                    64.0M      1.2M     62.8M   2% /tmp
tmpfs                     1.8G    208.0K      1.8G   0% /dev/shm
tmpfs                    16.0M      4.0K     16.0M   0% /share
/dev/mmcblk0p5            7.7M     52.0K      7.7M   1% /mnt/boot_config
tmpfs                    16.0M         0     16.0M   0% /mnt/snapshot/export
/dev/md9                493.5M    240.6M    252.9M  49% /mnt/HDA_ROOT
cgroup_root               1.8G         0      1.8G   0% /sys/fs/cgroup
/dev/mapper/vg1-lv1312
                        352.1M    232.0K    351.9M   0% /mnt/pool1
/dev/mapper/cachedev1
                          3.5T    642.6G      2.9T  18% /share/CACHEDEV1_DATA
/dev/md13               417.0M    381.7M     35.3M  92% /mnt/ext
tmpfs                    32.0M     27.2M      4.8M  85% /samba_third_party
tmpfs                     1.0M         0      1.0M   0% /share/external/.nd
tmpfs                     1.0M         0      1.0M   0% /share/external/.cm
tmpfs                     1.0M         0      1.0M   0% /mnt/hm/temp_mount
cmfs                      1.7T         0      1.7T   0% /share/external/.cm/0/1b7385b24-4af8-4481-a288-5f1e61a03cee
cmfs                      1.7T         0      1.7T   0% /share/external/.cm/0/1d6888667-4502-4d4f-ac2e-c47b4e3e2344
tmpfs                    48.0M     40.0K     48.0M   0% /share/CACHEDEV1_DATA/.samba/lock/msg.lock
tmpfs                    16.0M         0     16.0M   0% /mnt/ext/opt/samba/private/msg.sock
tmpfs                    10.0M         0     10.0M   0% /tmp/wfm
/dev/mapper/vg1-snap10002
                          3.5T    638.7G      2.9T  18% /mnt/snapshot/1/10002
/dev/mapper/vg1-snap10003
                          3.5T    639.3G      2.9T  18% /mnt/snapshot/1/10003
tmpfs                    64.0M      1.2M     62.8M   2% /share/CACHEDEV1_DATA/.qpkg/MediaSignPlayer/CodexPackExt/tmp
hdsfusemnt              400.0M    347.4M     52.6M  87% /share/CACHEDEV1_DATA/.qpkg/MediaSignPlayer/CodexPackExt/share
none                      1.8G         0      1.8G   0% /share/CACHEDEV1_DATA/.qpkg/MediaSignPlayer/CodexPackExt/sys/fs/cgroup
udev                      1.8G      4.0K      1.8G   0% /share/CACHEDEV1_DATA/.qpkg/MediaSignPlayer/CodexPackExt/dev
none                      1.8G         0      1.8G   0% /share/CACHEDEV1_DATA/.qpkg/MediaSignPlayer/CodexPackExt/dev/shm
tmpfs                     1.8G         0      1.8G   0% /share/CACHEDEV1_DATA/.qpkg/MediaSignPlayer/CodexPackExt/run
none                      1.8G         0      1.8G   0% /share/CACHEDEV1_DATA/.qpkg/MediaSignPlayer/CodexPackExt/run/lock
none                      1.8G         0      1.8G   0% /share/CACHEDEV1_DATA/.qpkg/MediaSignPlayer/CodexPackExt/run/user
tmpfs                   100.0K         0    100.0K   0% /share/CACHEDEV1_DATA/Container/container-station-data/lib/lxd/shmounts

df -h 找到大的空间   我选中 /share/CACHEDEV1_DATA, 这里有2.9TB,如下图

用命令生成一个全0的4GB文件

[/share/CACHEDEV1_DATA] # dd if=/dev/zero of=/share/CACHEDEV1_DATA/swapfile bs=1M count=4096
4096+0 records in
4096+0 records out
4294967296 bytes (4.0GB) copied, 20.826660 seconds, 196.7MB/s
  • if=/dev/zero:指定输入文件为 /dev/zero,用于生成零值字节的特殊设备文件。
  • of=/share/CACHEDEV1_DATA/swapfile 指定输出文件路径。
  • bs=1M count=4096:创建一个 4GB 大小的文件,每块 1MB,共 4096 个块。
[/share/CACHEDEV1_DATA] # ls -l
total 4194400
-rw-------  1 admin administrators       9216 2022-01-18 05:00 aquota.user
drwxrwxrwx  4 admin administrators       4096 2022-12-14 07:48 Container/
drwxrwxrwx 13 admin administrators       4096 2024-09-29 17:55 Download/
drwxrwxrwx 10 admin administrators       4096 2024-08-03 17:17 homes/
drwx------  2 admin administrators      16384 2020-12-12 07:57 lost+found/
drwxrwxrwx 20 admin administrators       4096 2024-10-05 01:17 Multimedia/
drwxrwxrwx  5 admin administrators       4096 2024-10-05 21:53 Public/
drwxrwxrwx  4 admin administrators       4096 2023-06-13 19:58 QVRCenterAppData/
drwxrwxrwx  5 admin administrators       4096 2023-06-13 00:45 QVRProAppData/
drwxrwxrwx  3 admin administrators       4096 2023-06-12 01:57 QVRProAutoSnap/
drwxrwxrwx  7 admin administrators       4096 2023-07-15 19:45 QVRProDB/
drwxrwxrwx  4 admin administrators       4096 2023-06-12 01:59 QVRProRecording/
drwxrwxrwx  5 admin administrators       4096 2023-12-15 19:40 Shard/
drwxr-xr-x 15 admin administrators       4096 2024-10-05 21:57 _.share/
drwxrwxrwx  9 admin administrators       4096 2024-08-19 19:09 Storage-Bank/
-rw-r--r--  1 admin administrators 4294967296 2024-10-05 23:12 swapfile
drwxrwxrwx  6 admin administrators       4096 2024-10-05 23:15 Web/
[/share/CACHEDEV1_DATA] # 

可以在上面看到: -rw-r--r--  1 admin administrators 4294967296 2024-10-05 23:12 swapfile

它只需要系统来读取就行,所以用chmod 600 修改权限,见下面:

[/share/CACHEDEV1_DATA] # chmod 600 /share/CACHEDEV1_DATA/swapfile
[/share/CACHEDEV1_DATA] # ls -l
total 4194400
-rw-------  1 admin administrators       9216 2022-01-18 05:00 aquota.user
drwxrwxrwx  4 admin administrators       4096 2022-12-14 07:48 Container/
drwxrwxrwx 13 admin administrators       4096 2024-09-29 17:55 Download/
drwxrwxrwx 10 admin administrators       4096 2024-08-03 17:17 homes/
drwx------  2 admin administrators      16384 2020-12-12 07:57 lost+found/
drwxrwxrwx 20 admin administrators       4096 2024-10-05 01:17 Multimedia/
drwxrwxrwx  5 admin administrators       4096 2024-10-05 21:53 Public/
drwxrwxrwx  4 admin administrators       4096 2023-06-13 19:58 QVRCenterAppData/
drwxrwxrwx  5 admin administrators       4096 2023-06-13 00:45 QVRProAppData/
drwxrwxrwx  3 admin administrators       4096 2023-06-12 01:57 QVRProAutoSnap/
drwxrwxrwx  7 admin administrators       4096 2023-07-15 19:45 QVRProDB/
drwxrwxrwx  4 admin administrators       4096 2023-06-12 01:59 QVRProRecording/
drwxrwxrwx  5 admin administrators       4096 2023-12-15 19:40 Shard/
drwxr-xr-x 15 admin administrators       4096 2024-10-05 21:57 _.share/
drwxrwxrwx  9 admin administrators       4096 2024-08-19 19:09 Storage-Bank/
-rw-------  1 admin administrators 4294967296 2024-10-05 23:12 swapfile
drwxrwxrwx  6 admin administrators       4096 2024-10-05 23:15 Web/
[/share/CACHEDEV1_DATA] # 

把它格式化成交换空间,老unix命令,我用的第一台UNIX是 Siemens Nixdorf 塔式机+oracle 7. 暴露年纪了。后来管过 SunOS, 这些证都是塑料垃圾了。

[/share/CACHEDEV1_DATA] # mkswap /share/CACHEDEV1_DATA/swapfile
Setting up swapspace version 1, size = 4294963 kB
[/share/CACHEDEV1_DATA] # 

启用交换文件

[/share/CACHEDEV1_DATA] # swapon /share/CACHEDEV1_DATA/swapfile
[/share/CACHEDEV1_DATA] # 

验证交换是否启用成功

[/share/CACHEDEV1_DATA] # swapon --show
swapon: unrecognized option '--show'
BusyBox v1.24.1 (2024-08-17 02:09:18 CST) multi-call binary.

Usage: swapon [-a] [-e] [-d[POL]] [-p PRI] [DEVICE]

Start swapping on DEVICE

        -a      Start swapping on all swap devices
        -d[POL] Discard blocks at swapon (POL=once),
                as freed (POL=pages), or both (POL omitted)
        -e      Silently skip devices that do not exist
        -p PRI  Set swap device priority
[/share/CACHEDEV1_DATA] # free -h
BusyBox v1.24.1 (2024-08-17 02:09:18 CST) multi-call binary.

Usage: free [-b/k/m/g]

Display the amount of free and used system memory
[/share/CACHEDEV1_DATA] # free -g
             total       used       free     shared    buffers     cached
Mem:             3          3          0          0          0          0
-/+ buffers/cache:          2          1
Swap:           34         16         17
[/share/CACHEDEV1_DATA] # cat /proc/swaps
Filename                                Type            Size            Used            Priority
/dev/md321                              partition       7751228         7750340         -2
/dev/md256                              partition       530108          512896          -3
/dev/md322                              partition       6702652         5444144         -4
/share/CACHEDEV1_DATA/.swap/qnap_swap   file            16777212        3430804         -5
/share/CACHEDEV1_DATA/swapfile          file            4194300         0               -6
[/share/CACHEDEV1_DATA] # 

开始,用swapon --show,提示没有这参数,(无法辨识 --show)。 又用了 free -h, 没看明白???怎么是34GB。  问CHATGPT:swapon 版本是 BusyBox 提供的精简版,因此不支持 --show 选项。它建议看 /proc/swaps

如果NAS重启,命令添加的swapfile就会在下次消失。 你可以考虑把它加到起动里。

比如 /etc/init.d/

放在 /etc/config/crontab 里

后续问题

性能
翻译10页laptop明显快于NAS数倍。
只把pdf转为txt,然后用 chrome那些翻译插件,可能效率跟高。
分解PDF为多个,并多线程运行,再把结果拼接,效率会提高。