基于网络api构建一个笔迹清除的web应用

enter image description here

Man can do what he wants but he cannot want what he wants - 亚瑟·叔本华 "人虽然能够做他所想做的,但不能要他所想要的"

Python,Flask, Bootstrap5 基于网络api构建一个笔迹清除应用

主要目地是方便普通用户使用: - 功能单一 - 流程清晰 - 使用无门槛

擦除试卷手写笔迹的web api

我选择 https://www.textin.com/document/text_auto_removal 根据官网的示例代码,我选择python版本:

import requests
import json

def get_file_content(filePath):
    with open(filePath, 'rb') as fp:
        return fp.read()

class CommonOcr(object):
    def __init__(self, img_path):
        # 请登录后前往 “工作台-账号设置-开发者信息” 查看 x-ti-app-id
        # 示例代码中 x-ti-app-id 非真实数据
        self._app_id = 'c81f*************************e9ff'
        # 请登录后前往 “工作台-账号设置-开发者信息” 查看 x-ti-secret-code
        # 示例代码中 x-ti-secret-code 非真实数据
        self._secret_code = '5508***********************1c17'
        self._img_path = img_path

    def recognize(self):
        # 自动擦除手写文字
        url = 'https://api.textin.com/ai/service/v1/handwritten_erase'
        head = {}
        try:
            image = get_file_content(self._img_path)
            head['x-ti-app-id'] = self._app_id
            head['x-ti-secret-code'] = self._secret_code
            result = requests.post(url, data=image, headers=head)
            return result.text
        except Exception as e:
            return e

if __name__ == "__main__":
    response = CommonOcr(r'example.jpg')
    print(response.recognize())

安装python环境,建立一个虚拟环境venv

    python3 -m venv myvenv

激活虚拟环境

    source myvenv/bin/activate  or myvenv\script\activate.bat

## 安装必要的package

     python -m pip install requests
     python -m pip install flask

以下是一个使用 Python、Flask 和 Bootstrap 5 构建的简单 Web 应用的参考实现,能够让用户上传试卷文件并通过 API 擦除笔迹。这个实现主要展示了文件上传和处理的流程,API 调用部分你可以根据实际的擦除笔迹 API 来集成。

exam_marks_eraser/
│
├── app.py  # 主应用文件
├── static/  # 静态文件(如 CSS、图片等)
│   └── styles.css
└── templates/
    ├── index.html  # 主页面
    └── result.html  # 结果页面

app.py代码处理文件上传,api处理,以及显示已经上传和处理的文件列表

import os
from flask import Flask, render_template, request, redirect, url_for, send_from_directory, flash, send_file
import requests
from dotenv import load_dotenv
import base64
import json
from celery import Celery
import time



app = Flask(__name__)
# 配置 Celery
app.config['CELERY_BROKER_URL'] = 'redis://localhost:6379/0'  # Redis作为消息代理
app.config['CELERY_RESULT_BACKEND'] = 'redis://localhost:6379/0'

celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'])
celery.conf.update(app.config)

app.config['UPLOAD_FOLDER'] = 'uploads/'
# PROCESSED_FOLDER = 'processed/'  # 处理后的文件存储目录
app.config['PROCESSED_FOLDER'] = 'processed/'

app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 限制文件大小为16MB
app.secret_key = 'supersecretkey'

def get_file_content(filePath):
    with open(filePath, 'rb') as fp:
        return fp.read()

class CommonOcr(object):
    def __init__(self, img_path):
        # 请登录后前往 “工作台-账号设置-开发者信息” 查看 x-ti-app-id

        load_dotenv()  # 加载 .env 文件
        self._app_id = os.getenv('APP_ID')
        self._secret_code = os.getenv('SECRET_CODE')
        # 示例代码中 x-ti-app-id 非真实数据
        # self._app_id = 'd19ecaa770409041a6ae2d392bcf566c'
        # 请登录后前往 “工作台-账号设置-开发者信息” 查看 x-ti-secret-code
        # 示例代码中 x-ti-secret-code 非真实数据
        # self._secret_code = '629f242b0f05e685c95f8994558c7a1e'
        self._img_path = img_path

    def recognize(self):
        # 自动擦除手写文字
        url = 'https://api.textin.com/ai/service/v1/handwritten_erase'
        head = {}
        try:
            image = get_file_content(self._img_path)
            head['x-ti-app-id'] = self._app_id
            head['x-ti-secret-code'] = self._secret_code
            result = requests.post(url, data=image, headers=head)
            return result.text
        except Exception as e:
            return e

def process_image(input_file):
    # 假设CommonOcr是一个定义好的函数或类,负责图像识别
    response = CommonOcr(input_file)
    result = response.recognize()
    # print(result)
    json_response = json.loads(result)

    image_data_base64 = json_response['result']['image']
    image_data = base64.b64decode(image_data_base64)

    # 生成输出文件名,添加后缀xxxx_output
    base, ext = os.path.splitext(input_file)
    output_file = f"{base}_output{ext}"

    with open(output_file, 'wb') as file:
        file.write(image_data)

    print(f"Output saved to {output_file}")


# 创建上传文件夹
if not os.path.exists(app.config['UPLOAD_FOLDER']):
    os.makedirs(app.config['UPLOAD_FOLDER'])

if not os.path.exists(app.config['PROCESSED_FOLDER']):
    os.makedirs(app.config['PROCESSED_FOLDER'])

def erase_marks(file_path):
    """
    通过 API 擦除文件中的笔迹。
    这里只是一个伪函数,你需要用真实 API 来替换。
    """
    response = CommonOcr(file_path)
    result = response.recognize()
    json_response = json.loads(result)

    if json_response['code'] == 200:
        image_data_base64 = json_response['result']['image']
        image_data = base64.b64decode(image_data_base64)
        output_file_path = os.path.join(app.config['PROCESSED_FOLDER'], 'cleaned_' + os.path.basename(file_path))
        with open(output_file_path, 'wb') as f:
            f.write(image_data)
        return output_file_path
    else:
        flash('处理失败,请稍后重试。')
        return None

@app.route('/', methods=['GET', 'POST'])
def index():
    # 获取已上传的文件
    uploaded_files = os.listdir(app.config['UPLOAD_FOLDER'])
    processed_files = os.listdir(app.config['PROCESSED_FOLDER'])

    if request.method == 'POST':
        # 检查是否上传了文件
        if 'file' not in request.files:
            flash('没有文件上传')
            return redirect(request.url)

        file = request.files['file']
        if file.filename == '':
            flash('没有选择文件', 'warning')

            return redirect(request.url)

        if file:
            file_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
            file.save(file_path)

            # 通过 API 处理文件
            processed_file_path = erase_marks(file_path)

            if processed_file_path:
                return redirect(url_for('result', filename=os.path.basename(processed_file_path)))

    return render_template('index.html', uploaded_files=uploaded_files, processed_files=processed_files)

@app.route('/result/<filename>')
def result(filename):
    return render_template('result.html', filename=filename)

@app.route('/uploads/<filename>')
def uploaded_file(filename):
    #return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
    file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
    if os.path.exists(file_path):
        # 返回带有附件标志的文件
        return send_file(file_path, as_attachment=True)
    else:
        flash('文件未找到!')
        return redirect(url_for('index'))

# 已处理文件的下载路由
@app.route('/processed/<filename>')
def download_processed_file(filename):
    file_path = os.path.join(app.config['PROCESSED_FOLDER'], filename)
    if os.path.exists(file_path):
        # 返回带有附件标志的文件
        return send_file(file_path, as_attachment=True)
    else:
        flash('文件未找到!')
        return redirect(url_for('index'))
    # return send_from_directory(app.config['PROCESSED_FOLDER'], filename)

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

页面风格

为了让页面拥有现代、简约并带有科技未来感的风格,你可以使用较深的背景色调(如深蓝或黑色),搭配流畅的线条和渐变效果,使用光滑的按钮、阴影效果以及酷炫的字体样式。以下是 static/styles.css 的一个参考样式:

  • static/styles.css
    /* 全局样式 */
    body {
        background-color: #0e1d36; /* 深蓝色背景,科技感 */
        color: #ffffff; /* 文字颜色为白色,突出与背景的对比 */
        font-family: 'Roboto', sans-serif; /* 使用科技感较强的字体 */
        margin: 0;
        padding: 0;
    }

    h1 {
        font-weight: 300;
        text-transform: uppercase;
        letter-spacing: 2px;
        color: #00d4ff; /* 明亮的青色,带来未来感 */
        text-shadow: 0 0 5px rgba(0, 212, 255, 0.6);
    }

    .container {
        max-width: 600px;
        margin: 100px auto;
        padding: 20px;
        background: #16243b; /* 深色背景容器,区分主体与背景 */
        border-radius: 10px;
        box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
    }

    /* 表单样式 */
    input[type="file"] {
        background: transparent;
        color: #00d4ff;
        border: 2px dashed #00d4ff; /* 虚线边框,科技感十足 */
        padding: 10px;
        width: 100%;
        border-radius: 10px;
        transition: border 0.3s ease;
    }

    input[type="file"]:hover {
        border: 2px dashed #00f2ff; /* hover 时的边框颜色变亮 */
    }

    button {
        background: linear-gradient(90deg, #00d4ff, #0072ff); /* 渐变按钮 */
        color: #ffffff;
        border: none;
        padding: 12px;
        width: 100%;
        border-radius: 50px; /* 圆滑按钮,现代感 */
        font-size: 16px;
        text-transform: uppercase;
        letter-spacing: 1px;
        box-shadow: 0 8px 20px rgba(0, 212, 255, 0.4); /* 按钮光影效果 */
        transition: background 0.3s ease;
    }

    button:hover {
        background: linear-gradient(90deg, #0072ff, #00d4ff); /* 鼠标悬停时改变渐变方向 */
        box-shadow: 0 12px 25px rgba(0, 212, 255, 0.7);
    }

    /* 链接按钮 */
    a.btn-success {
        background: linear-gradient(90deg, #00ff99, #00d4ff); /* 下载按钮也有渐变 */
        border-radius: 50px;
        text-transform: uppercase;
        letter-spacing: 1px;
        padding: 12px 20px;
        color: white;
        text-decoration: none;
        display: inline-block;
        box-shadow: 0 8px 20px rgba(0, 255, 153, 0.4);
        transition: background 0.3s ease;
    }

    a.btn-success:hover {
        background: linear-gradient(90deg, #00d4ff, #00ff99);
        box-shadow: 0 12px 25px rgba(0, 255, 153, 0.7);
    }

    /* 提示信息样式 */
    .alert-danger {
        background-color: #ff3b3b;
        border: none;
        border-radius: 5px;
        color: white;
        padding: 10px;
        text-align: center;
        box-shadow: 0 4px 15px rgba(255, 59, 59, 0.5);
    }


    /* 其他样式保持不变 */

    /* 为 btn-secondary 按钮添加自定义样式 */
    .btn-secondary {
        background: linear-gradient(90deg, #6c757d, #5a6268); /* 渐变背景色 */
        color: #ffffff; /* 文字颜色为白色 */
        border: none; /* 去掉默认边框 */
        padding: 12px 20px; /* 添加内边距 */
        border-radius: 50px; /* 圆滑按钮边角 */
        font-size: 16px; /* 字体大小 */
        text-transform: uppercase; /* 全部大写 */
        letter-spacing: 1px; /* 字符间距 */
        box-shadow: 0 8px 20px rgba(108, 117, 125, 0.4); /* 添加阴影效果 */
        transition: background 0.3s ease, box-shadow 0.3s ease; /* 平滑过渡效果 */
    }

    .btn-secondary:hover {
        background: linear-gradient(90deg, #5a6268, #6c757d); /* 鼠标悬停时的渐变背景色变化 */
        box-shadow: 0 12px 25px rgba(108, 117, 125, 0.7); /* 鼠标悬停时的阴影效果变化 */
    }

    .btn-secondary:focus, .btn-secondary:active {
        outline: none; /* 去掉焦点轮廓 */
        box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); /* 修改焦点时的阴影效果 */
    }


    /* 自定义底部样式 */
    .footer {
        position: fixed;
        bottom: 0;
        left: 0;
        width: 100%;
        background-color: #16243b;
        color: white;
        text-align: center; /* 居中对齐文本 */
        padding: 10px;
        font-size: 14px;
        box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.2); /* 可选:添加阴影 */
    }
    .footer-content {
        display: flex;
        flex-direction: column;
        align-items: center;
    }
    .footer a {
        color: #ffffff;
        text-decoration: none;
    }
    .footer a:hover {
        text-decoration: underline;
    }


    /* 自定义背景色 */
    .list-group-item {
        background-color: #343a40; /* 深色背景 */
        color: #f8f9fa; /* 浅色字体 */
        border: 1px solid #495057; /* 深色边框 */
    }

    /* 自定义悬停效果 */
    .list-group-item:hover {
        background-color: #495057; /* 悬停时的深色背景 */
        color: #f8f9fa; /* 悬停时的浅色字体 */
    }

    /* 自定义文件上传控件样式 */
    .custom-file-input {
        background-color: #343a40; /* 深色背景 */
        color: #f8f9fa; /* 浅色文字 */
        border: 1px solid #495057; /* 深色边框 */
        border-radius: .375rem; /* 圆角 */
    }

    /* 自定义文件选择按钮的样式 */
    .custom-file-input::-webkit-file-upload-button {
        background-color: #495057; /* 深色按钮背景 */
        color: #f8f9fa; /* 按钮文字颜色 */
        border: none; /* 去掉边框 */
        padding: .375rem .75rem; /* 内边距 */
        border-radius: .375rem; /* 圆角 */
    }

    /* 在 Firefox 和其他浏览器中应用自定义样式 */
    .custom-file-input::file-selector-button {
        background-color: #495057;
        color: #f8f9fa;
        border: none;
        padding: .375rem .75rem;
        border-radius: .375rem;
    }

    .custom-file-input:hover {
        background-color: #495057; /* 鼠标悬停时背景色 */
        border-color: #6c757d; /* 鼠标悬停时边框色 */
    }


    /* 自定义 Flash 消息样式 */
    .alert {
        border-radius: .375rem; /* 圆角边框 */
        padding: .75rem 1.25rem; /* 内边距 */
        margin-bottom: 1rem; /* 下边距 */
    }

    .alert-success {
        background-color: #28a745; /* 成功消息背景色 */
        color: #f8f9fa; /* 成功消息文字颜色 */
        border-color: #1e7e34; /* 成功消息边框颜色 */
    }

    .alert-danger {
        background-color: #dc3545; /* 错误消息背景色 */
        color: #f8f9fa; /* 错误消息文字颜色 */
        border-color: #c82333; /* 错误消息边框颜色 */
    }

    .alert-info {
        background-color: #17a2b8; /* 信息消息背景色 */
        color: #f8f9fa; /* 信息消息文字颜色 */
        border-color: #117a8b; /* 信息消息边框颜色 */
    }

    .alert-warning {
        background-color: #ffc107; /* 警告消息背景色 */
        color: #212529; /* 警告消息文字颜色 */
        border-color: #e0a800; /* 警告消息边框颜色 */
    }

    .alert-dismissible .btn-close {
        filter: invert(1); /* 使关闭按钮的颜色与背景颜色协调 */
    }

页面模板

  • index.html
  • result.html

index.html首页页面,有个文件选择控件,列出已经上传的文件,以及处理好的文件列表链接,可以直接点击下载

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>试卷笔迹擦除工具</title>
    <!-- 引入 Bootstrap 样式 -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
    <!-- 引入自定义的 CSS 文件 -->
    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">

</head>
<body>
    <div class="container mt-5">
        <h1 class="text-center">上传试卷文件</h1>

        <!-- Flash Messages -->
        {% with messages = get_flashed_messages(with_categories=true) %}
            {% if messages %}
                <div class="alert-container">
                    {% for category, message in messages %}
                        <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
                            {{ message }}
                            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
                        </div>
                    {% endfor %}
                </div>
            {% endif %}
        {% endwith %}

        <!-- 文件上传表单 -->
        <form action="/" method="post" enctype="multipart/form-data">
            <div class="mb-3">
                <!-- <label for="file-input" class="form-label">Choose file</label> -->
                <input class="custom-file-input" type="file" name="file" required>
            </div>
            <button type="submit" class="btn btn-primary w-100">上传并处理</button>
        </form>

        <!-- 已上传文件列表 -->
        <h2 class="mt-5">已上传文件列表</h2>
        {% if uploaded_files %}
        <ul class="list-group">
            {% for file in uploaded_files %}
            <li class="list-group-item">
                {{ file }}
            </li>
            {% endfor %}
        </ul>
        {% else %}
        <p>尚未上传文件</p>
        {% endif %}

        <!-- 已处理文件列表 -->
        <h2 class="mt-5">已处理文件列表</h2>
        {% if processed_files %}
        <ul class="list-group">
            {% for file in processed_files %}
            <li class="list-group-item">
                <a href="{{ url_for('download_processed_file', filename=file) }}">{{ file }}</a>
            </li>
            {% endfor %}
        </ul>
        {% else %}
        <p>尚未处理文件</p>
        {% endif %}

    </div>

    <footer class="footer">
        <div class="footer-content">
            <p>© 2024 你的公司名. 版权所有.</p>
            <p>联系方式: <a href="mailto:contact@example.com">contact@example.com</a></p>
        </div>
    </footer>
</body>
</html>

result.html,结果页面,处理完成后,重定向到结果页面,用户可以下载擦除笔迹后的文件;也可以点击直接返回主页,继续上传新文件或者点击下载已经处理好的文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>处理结果</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">

</head>
<body>
    <div class="container mt-5">
        <h1 class="text-center">处理完成</h1>

        <div class="text-center">
            <a href="{{ url_for('download_processed_file', filename=filename) }}" class="btn btn-success mt-3">下载处理后的文件</a>
            <a href="{{ url_for('index') }}" class="btn btn-secondary mt-3">返回上传页面</a>
        </div>
    </div>

    <footer class="footer">
        <div class="footer-content">
            <p>© 2024 你的公司名. 版权所有.</p>
            <p>联系方式: <a href="mailto:contact@example.com">contact@example.com</a></p>
        </div>
    </footer>
</body>
</html>

运行效果

enter image description here - 运行flask app 简单做法: python app.py

或者

export FLASK_APP=app.py
python -m flask run --host 0.0.0.0

注意事项 需要定义一个.env文件,将自己申请的web api的key 和id放在里面,调用web api的时候需要用到

在生产环境部署,使用gunicorn

在生产环境中部署 Flask 应用需要使用一个稳定、安全的配置,而不仅仅依赖于开发环境中的 flask run。生产环境通常包括一个实际的 Web 服务器和一个 WSGI 服务器,以确保应用能够处理多个并发请求,并提供适当的安全性和性能。

部署 Flask 应用的步骤:

  1. 使用 WSGI 服务器(如 Gunicorn 或 uWSGI) Flask 内置的开发服务器不适合生产环境。你需要使用一个 WSGI 兼容的服务器,如 Gunicorn 或 uWSGI。Gunicorn 是一种轻量级的、多线程的 WSGI HTTP 服务器,适合生产环境。

  2. 选择 Web 服务器(如 Nginx 或 Apache) Nginx 或 Apache 是常用的 Web 服务器,用于反向代理并提供静态文件的服务。Nginx 更常用于现代的 Flask 部署中。

  3. 配置和部署步骤

- Step 1: 安装依赖 在你的生产服务器上,你需要首先确保 Python 及相关依赖已安装。

        sudo apt update
        sudo apt install python3-pip python3-venv nginx
  • Step 2: 创建 Python 虚拟环境并安装依赖 在生产服务器上,为 Flask 应用创建虚拟环境,并安装所需的 Python 包。
cd /path/to/your/flask/app
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
  • Step 3: 安装并配置 Gunicorn 安装 Gunicorn 来处理 WSGI 请求:
    pip install gunicorn

使用以下命令测试 Gunicorn 运行 Flask 应用:

    gunicorn --bind 0.0.0.0:8000 wsgi:app

这里假设你的 Flask 应用文件叫 wsgi.py,其中定义了 app 作为 Flask 实例。

  • Step 4: 配置 Nginx 作为反向代理 在生产环境中,你通常会使用 Nginx 来处理客户端请求并将它们代理到 Gunicorn。

安装 Nginx:

    sudo apt install nginx

配置 Nginx:

创建一个新的 Nginx 配置文件:

    sudo nano /etc/nginx/sites-available/myflaskapp

文件配置内容:

server {
    listen 80;
    server_name your_domain_or_IP;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /static/ {
        alias /path/to/your/flask/app/static/;
    }
}

确保将 /path/to/your/flask/app/static/ 替换为你的 Flask 项目中静态文件的路径。 启用 Nginx 配置:

创建一个指向新配置的符号链接,并重新加载 Nginx:

    sudo ln -s /etc/nginx/sites-available/myflaskapp /etc/nginx/sites-enabled
    sudo systemctl restart nginx
  • Step 5: 配置 Gunicorn 作为服务

为了让 Gunicorn 在系统启动时自动运行,可以将其配置为一个 systemd 服务。

创建一个新的服务文件:

sudo nano /etc/systemd/system/myflaskapp.service

在该文件中,添加以下内容:

[Unit]
Description=Gunicorn instance to serve myflaskapp
After=network.target

[Service]
User=youruser
Group=www-data
WorkingDirectory=/path/to/your/flask/app
ExecStart=/path/to/your/flask/app/venv/bin/gunicorn --workers 3 --bind 127.0.0.1 wsgi:app

[Install]
WantedBy=multi-user.target

假定应用是wsgi.py,里面定义了app 启动并启用服务:

    sudo systemctl start myflaskapp
    sudo systemctl enable myflaskapp

使用socket通信没有搞定,暂时用ip端口绑定通信

评论