PPT/8.0/server.py
2025-05-14 17:49:03 +08:00

498 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from flask import Flask, request, jsonify, render_template, send_from_directory
from flask_cors import CORS
import os
import traceback
import logging
import ctypes
import shutil
import threading
import time
import uuid
import json
from concurrent.futures import ThreadPoolExecutor
from werkzeug.utils import secure_filename
from 毕设 import main_process
# 导入补丁模块 - 用于解决wkhtmltopdf依赖问题
try:
import pdfkit_patch as pdfkit
print("已加载pdfkit补丁模块")
except ImportError:
print("未找到pdfkit补丁模块PDF生成功能可能不可用")
# 配置日志
log_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'server.log')
logger = logging.getLogger('server')
logger.setLevel(logging.DEBUG)
# 创建文件处理器指定编码为utf-8
file_handler = logging.FileHandler(log_file, encoding='utf-8', mode='w')
file_handler.setLevel(logging.DEBUG)
# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
# 创建格式化器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# 添加处理器到logger
logger.addHandler(file_handler)
logger.addHandler(console_handler)
# 确保日志文件存在
if not os.path.exists(log_file):
with open(log_file, 'w', encoding='utf-8') as f:
f.write('')
app = Flask(__name__)
CORS(app) # 启用CORS支持
# 配置上传文件夹
UPLOAD_FOLDER = os.path.abspath('uploads')
OUTPUT_FOLDER = os.path.abspath('output')
TASK_STATUS_FILE = os.path.join(OUTPUT_FOLDER, 'task_status.json')
# 创建必要的目录
for folder in [UPLOAD_FOLDER, OUTPUT_FOLDER]:
if not os.path.exists(folder):
os.makedirs(folder)
logger.info(f"创建目录: {folder}")
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['OUTPUT_FOLDER'] = OUTPUT_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 512 * 1024 * 1024 # 增加上传大小限制到512MB
ALLOWED_EXTENSIONS = {'mp4', 'avi', 'mov'}
# 创建线程池,限制并发处理任务数
MAX_WORKERS = 2 # 最多同时处理2个视频
executor = ThreadPoolExecutor(max_workers=MAX_WORKERS)
# 初始化任务状态存储
tasks = {}
if os.path.exists(TASK_STATUS_FILE):
try:
with open(TASK_STATUS_FILE, 'r', encoding='utf-8') as f:
tasks = json.load(f)
except:
logger.error("无法读取任务状态文件,创建新的状态跟踪")
tasks = {}
def save_task_status():
"""保存任务状态到文件"""
try:
with open(TASK_STATUS_FILE, 'w', encoding='utf-8') as f:
json.dump(tasks, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"保存任务状态失败: {str(e)}")
def get_free_space_gb(dirname):
"""获取指定目录所在磁盘的可用空间GB"""
free_bytes = ctypes.c_ulonglong(0)
ctypes.windll.kernel32.GetDiskFreeSpaceExW(
ctypes.c_wchar_p(dirname), None, None, ctypes.pointer(free_bytes)
)
return free_bytes.value / 1024 / 1024 / 1024 # 转换为GB
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def process_video_async(task_id, filepath, filename):
"""异步处理视频的函数"""
try:
# 更新任务状态为处理中
tasks[task_id]['status'] = 'processing'
tasks[task_id]['progress'] = 10
save_task_status()
# 处理视频文件
logger.info(f"[任务 {task_id}] 开始处理视频: {filename}")
# 创建任务专属的输出目录
task_output_dir = os.path.join(app.config['OUTPUT_FOLDER'], task_id)
os.makedirs(task_output_dir, exist_ok=True)
# 设置进度回调
def progress_callback(progress, message=None):
tasks[task_id]['progress'] = progress
if message:
tasks[task_id]['message'] = message
save_task_status()
# 处理视频
start_time = time.time()
result = main_process(filepath, output_dir=task_output_dir, progress_callback=progress_callback)
end_time = time.time()
if result:
# 处理成功
tasks[task_id]['status'] = 'completed'
tasks[task_id]['progress'] = 100
tasks[task_id]['message'] = '处理完成'
# 修改报告路径格式,确保前端能正确访问
# 检查报告文件是否存在
html_path = os.path.join(task_output_dir, "summary.html")
if os.path.exists(html_path):
# 使用专门的查看报告路由
tasks[task_id]['result_path'] = f"/view_report/{task_id}"
logger.info(f"[任务 {task_id}] 报告预览路径: {tasks[task_id]['result_path']}")
else:
# 查找是否有备用报告文件
backup_files = [f for f in os.listdir(task_output_dir) if f.endswith('.html')]
if backup_files:
tasks[task_id]['result_path'] = f"/view_report/{task_id}"
logger.info(f"[任务 {task_id}] 使用备用报告预览路径: {tasks[task_id]['result_path']}")
else:
# 创建一个简单的状态页面
simple_html = os.path.join(task_output_dir, "status.html")
with open(simple_html, "w", encoding="utf-8") as f:
f.write(f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>处理状态</title>
<style>
body {{ font-family: Arial, sans-serif; padding: 20px; max-width: 800px; margin: 0 auto; }}
.status {{ padding: 15px; background-color: #f8f9fa; border-radius: 5px; margin-top: 20px; }}
</style>
</head>
<body>
<h1>视频处理完成</h1>
<div class="status">
<p>视频已成功处理,但未找到标准报告文件。</p>
<p>请检查服务器日志以获取详细信息。</p>
<p>处理时间: {round(end_time - start_time, 2)}秒</p>
</div>
</body>
</html>
""")
tasks[task_id]['result_path'] = f"/view_report/{task_id}"
logger.info(f"[任务 {task_id}] 创建状态页面: {simple_html}")
tasks[task_id]['process_time'] = round(end_time - start_time, 2)
logger.info(f"[任务 {task_id}] 视频处理成功,耗时: {tasks[task_id]['process_time']}")
else:
# 处理失败
tasks[task_id]['status'] = 'failed'
tasks[task_id]['message'] = '处理失败,请检查日志'
logger.error(f"[任务 {task_id}] 视频处理失败")
save_task_status()
except Exception as e:
# 处理异常
logger.error(f"[任务 {task_id}] 处理过程中发生异常: {str(e)}")
try:
logger.error(traceback.format_exc())
except Exception:
logger.error("无法获取详细错误信息traceback模块不可用")
tasks[task_id]['status'] = 'failed'
tasks[task_id]['message'] = f'错误: {str(e)}'
save_task_status()
@app.route('/')
def index():
return render_template('index.html')
@app.route('/output/<path:filename>')
def serve_output(filename):
"""提供输出文件下载"""
try:
logger.info(f"请求输出文件: {filename}")
# 检查文件名是否包含任务ID路径
if '/' in filename:
task_id = filename.split('/')[0]
file_path = '/'.join(filename.split('/')[1:])
task_output_dir = os.path.join(app.config['OUTPUT_FOLDER'], task_id)
if not os.path.exists(task_output_dir):
logger.error(f"任务输出目录不存在: {task_output_dir}")
return f"任务输出目录不存在: {task_id}", 404
file_full_path = os.path.join(task_output_dir, file_path)
if not os.path.exists(file_full_path):
logger.error(f"请求的文件不存在: {file_full_path}")
return f"请求的文件不存在: {file_path}", 404
logger.info(f"提供文件: {file_full_path}")
return send_from_directory(task_output_dir, file_path)
else:
# 处理不包含任务ID的直接文件
output_dir = app.config['OUTPUT_FOLDER']
file_full_path = os.path.join(output_dir, filename)
if not os.path.exists(file_full_path):
logger.error(f"请求的文件不存在: {file_full_path}")
return f"请求的文件不存在: {filename}", 404
logger.info(f"提供文件: {file_full_path}")
return send_from_directory(output_dir, filename)
except Exception as e:
logger.error(f"提供输出文件时出错: {str(e)}")
return f"服务器错误: {str(e)}", 500
@app.route('/view_report/<task_id>')
def view_report(task_id):
"""提供报告预览页面"""
try:
if task_id not in tasks:
return "任务不存在", 404
task = tasks[task_id]
if task['status'] != 'completed':
return f"任务尚未完成,当前状态: {task['status']}", 400
task_output_dir = os.path.join(app.config['OUTPUT_FOLDER'], task_id)
html_path = os.path.join(task_output_dir, "summary.html")
if os.path.exists(html_path):
logger.info(f"提供报告预览: {html_path}")
with open(html_path, 'r', encoding='utf-8') as f:
html_content = f.read()
return html_content
else:
# 查找是否有备用报告文件
backup_files = [f for f in os.listdir(task_output_dir) if f.endswith('.html')]
if backup_files:
alt_html_path = os.path.join(task_output_dir, backup_files[0])
logger.info(f"提供备用报告预览: {alt_html_path}")
with open(alt_html_path, 'r', encoding='utf-8') as f:
html_content = f.read()
return html_content
else:
return "找不到报告文件", 404
except Exception as e:
logger.error(f"提供报告预览时出错: {str(e)}")
return f"服务器错误: {str(e)}", 500
@app.route('/api/upload', methods=['POST'])
def upload_file():
try:
logger.info("收到上传请求")
# 检查是否有文件被上传
if 'file' not in request.files:
logger.error("没有文件被上传")
return jsonify({'error': '没有文件被上传'}), 400
file = request.files['file']
if file.filename == '':
logger.error("没有选择文件")
return jsonify({'error': '没有选择文件'}), 400
if not allowed_file(file.filename):
logger.error(f"不支持的文件类型: {file.filename}")
return jsonify({'error': '不支持的文件类型请上传MP4、AVI或MOV格式的视频'}), 400
# 生成唯一任务ID
task_id = str(uuid.uuid4())
# 保存文件
original_filename = file.filename
safe_filename = secure_filename(original_filename)
filepath = os.path.join(app.config['UPLOAD_FOLDER'], f"{task_id}_{safe_filename}")
logger.info(f"[任务 {task_id}] 上传文件信息: 文件名={original_filename}, 大小={request.content_length}字节")
logger.info(f"[任务 {task_id}] 正在保存文件到: {filepath}")
# 检查磁盘空间
required_space = request.content_length / (1024 * 1024 * 1024) * 2 # 需要2倍文件大小的空间
free_space = get_free_space_gb(UPLOAD_FOLDER)
logger.info(f"[任务 {task_id}] 磁盘空间检查: 需要 {required_space:.2f}GB可用 {free_space:.2f}GB")
if free_space < required_space:
logger.error(f"[任务 {task_id}] 磁盘空间不足: 需要 {required_space:.2f}GB可用 {free_space:.2f}GB")
return jsonify({'error': '磁盘空间不足,请清理磁盘后重试'}), 400
# 保存任务状态
tasks[task_id] = {
'id': task_id,
'filename': original_filename,
'filepath': filepath,
'uploaded_at': time.strftime('%Y-%m-%d %H:%M:%S'),
'size': request.content_length,
'status': 'uploading',
'progress': 0,
'message': '正在上传文件...'
}
save_task_status()
try:
# 保存文件
file.save(filepath)
logger.info(f"[任务 {task_id}] 文件保存成功: {filepath}")
# 验证文件大小
saved_size = os.path.getsize(filepath)
size_diff = abs(request.content_length - saved_size)
logger.info(f"[任务 {task_id}] 文件大小验证: 上传={request.content_length}字节, 保存={saved_size}字节, 差异={size_diff}字节")
if size_diff > 1024: # 允许1KB的差异
try:
os.remove(filepath)
logger.info(f"[任务 {task_id}] 已删除不完整的文件: {filepath}")
except Exception as e:
logger.error(f"[任务 {task_id}] 删除不完整文件失败: {str(e)}")
tasks[task_id]['status'] = 'failed'
tasks[task_id]['message'] = '文件上传不完整,请重试'
save_task_status()
return jsonify({'error': '文件保存不完整,请重试', 'task_id': task_id}), 500
# 更新任务状态为等待处理
tasks[task_id]['status'] = 'pending'
tasks[task_id]['message'] = '等待处理...'
tasks[task_id]['progress'] = 5
save_task_status()
# 提交异步处理任务
executor.submit(process_video_async, task_id, filepath, original_filename)
# 返回任务ID
return jsonify({
'message': '文件上传成功,已加入处理队列',
'task_id': task_id
}), 200
except Exception as e:
logger.error(f"[任务 {task_id}] 保存或准备处理时出错: {str(e)}")
try:
logger.error(traceback.format_exc())
except Exception:
logger.error("无法获取详细错误信息traceback模块不可用")
tasks[task_id]['status'] = 'failed'
tasks[task_id]['message'] = f'错误: {str(e)}'
save_task_status()
return jsonify({'error': f'上传失败: {str(e)}', 'task_id': task_id}), 500
except Exception as e:
logger.error(f"上传过程中发生错误: {str(e)}")
try:
logger.error(traceback.format_exc())
except Exception:
logger.error("无法获取详细错误信息traceback模块不可用")
return jsonify({'error': f'上传失败: {str(e)}'}), 500
@app.route('/api/tasks', methods=['GET'])
def get_all_tasks():
"""获取所有任务的状态"""
return jsonify({'tasks': list(tasks.values())}), 200
@app.route('/api/tasks/<task_id>', methods=['GET'])
def get_task_status(task_id):
"""获取特定任务的状态"""
if task_id in tasks:
return jsonify(tasks[task_id]), 200
else:
return jsonify({'error': '任务不存在'}), 404
@app.route('/api/tasks/<task_id>/cancel', methods=['POST'])
def cancel_task(task_id):
"""取消任务"""
if task_id not in tasks:
return jsonify({'error': '任务不存在'}), 404
if tasks[task_id]['status'] in ['completed', 'failed']:
return jsonify({'error': '无法取消已完成或失败的任务'}), 400
# 更新任务状态
tasks[task_id]['status'] = 'cancelled'
tasks[task_id]['message'] = '任务已取消'
save_task_status()
# 清理文件
try:
if os.path.exists(tasks[task_id]['filepath']):
os.remove(tasks[task_id]['filepath'])
except Exception as e:
logger.error(f"删除已取消任务的文件失败: {str(e)}")
return jsonify({'message': '任务已取消'}), 200
@app.route('/api/tasks/<task_id>', methods=['DELETE'])
def delete_task(task_id):
"""删除任务"""
if task_id not in tasks:
return jsonify({'error': '任务不存在'}), 404
# 如果任务正在处理中,不允许删除
if tasks[task_id]['status'] in ['processing', 'uploading']:
return jsonify({'error': '无法删除正在处理中的任务,请先取消'}), 400
# 清理文件
try:
# 删除上传的文件
if os.path.exists(tasks[task_id]['filepath']):
os.remove(tasks[task_id]['filepath'])
logger.info(f"[任务 {task_id}] 已删除上传文件: {tasks[task_id]['filepath']}")
# 删除输出目录
task_output_dir = os.path.join(app.config['OUTPUT_FOLDER'], task_id)
if os.path.exists(task_output_dir):
shutil.rmtree(task_output_dir)
logger.info(f"[任务 {task_id}] 已删除输出目录: {task_output_dir}")
except Exception as e:
logger.error(f"[任务 {task_id}] 删除文件失败: {str(e)}")
# 从任务列表中删除
del tasks[task_id]
save_task_status()
return jsonify({'message': '任务已删除'}), 200
@app.route('/api/tasks/<task_id>/retry', methods=['POST'])
def retry_task(task_id):
"""重试失败的任务"""
if task_id not in tasks:
return jsonify({'error': '任务不存在'}), 404
# 只允许重试失败或已取消的任务
if tasks[task_id]['status'] not in ['failed', 'cancelled']:
return jsonify({'error': f"无法重试处于 {tasks[task_id]['status']} 状态的任务"}), 400
# 检查文件是否还存在
if not os.path.exists(tasks[task_id]['filepath']):
return jsonify({'error': '原始视频文件已不存在,无法重试'}), 400
# 更新任务状态
original_filepath = tasks[task_id]['filepath']
original_filename = tasks[task_id]['filename']
# 更新任务状态
tasks[task_id]['status'] = 'pending'
tasks[task_id]['progress'] = 5
tasks[task_id]['message'] = '已重新加入处理队列...'
tasks[task_id]['retried_at'] = time.strftime('%Y-%m-%d %H:%M:%S')
# 清理旧的输出目录
task_output_dir = os.path.join(app.config['OUTPUT_FOLDER'], task_id)
if os.path.exists(task_output_dir):
try:
shutil.rmtree(task_output_dir)
logger.info(f"[任务 {task_id}] 已清理旧的输出目录")
except Exception as e:
logger.error(f"[任务 {task_id}] 清理旧输出目录失败: {str(e)}")
# 保存任务状态
save_task_status()
# 提交异步处理任务
executor.submit(process_video_async, task_id, original_filepath, original_filename)
return jsonify({
'message': '任务已重新提交处理',
'task_id': task_id
}), 200
if __name__ == '__main__':
logger.info("启动服务器...")
app.run(debug=True, port=5000, threaded=True)