PPT/服务器启动/server.py

567 lines
22 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
# 首先尝试导入兼容层模块
try:
import pdfkit_patch as pdfkit
print("已加载pdfkit补丁模块")
except ImportError:
print("未找到pdfkit补丁模块尝试创建...")
# 创建一个简单的补丁模块
with open("pdfkit_patch.py", "w", encoding="utf-8") as f:
f.write("""
import logging
logger = logging.getLogger("pdfkit_patch")
def from_string(html_content, output_path, options=None):
html_output = output_path.replace('.pdf', '.html')
with open(html_output, 'w', encoding='utf-8') as f:
f.write(html_content)
return True
def from_file(input_file, output_file, options=None):
return True
""")
import pdfkit_patch as pdfkit
print("已创建并加载pdfkit补丁模块")
# 有条件地导入毕设.py中的函数
try:
from 毕设 import main_process
print("成功导入毕设.py处理函数")
except Exception as e:
print(f"警告: 导入毕设.py失败: {str(e)}")
print("将使用简化的处理流程...")
# 定义一个简化的替代函数
def main_process(video_path, output_dir=None, progress_callback=None):
"""简化的视频处理函数,用于在无法导入毕设.py时提供基本功能"""
print(f"使用简化处理函数处理视频: {video_path}")
if not os.path.exists(video_path):
print(f"错误: 视频文件不存在: {video_path}")
return False
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir, exist_ok=True)
# 模拟进度
for progress in range(0, 101, 10):
if progress_callback:
progress_callback(progress, f"处理中 {progress}%")
time.sleep(0.5) # 模拟处理时间
# 创建一个简单的HTML报告
if output_dir:
html_path = os.path.join(output_dir, "summary.html")
with open(html_path, "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; }}
h1 {{ color: #333; }}
</style>
</head>
<body>
<h1>简化视频处理报告</h1>
<p>视频文件: {os.path.basename(video_path)}</p>
<p>注意: 这是一个简化的处理报告,完整功能未启用。</p>
</body>
</html>
""")
return True
# 配置日志
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)
if not os.path.exists(task_output_dir):
return f"任务输出目录不存在: {task_id}", 404
# 查找HTML报告文件
html_files = [f for f in os.listdir(task_output_dir) if f.endswith('.html')]
if not html_files:
return f"未找到任务 {task_id} 的报告文件", 404
# 优先使用summary.html
if "summary.html" in html_files:
report_file = "summary.html"
else:
report_file = html_files[0]
logger.info(f"[任务 {task_id}] 提供报告预览: {report_file}")
return send_from_directory(task_output_dir, report_file)
except Exception as e:
logger.error(f"提供报告预览时出错: {str(e)}")
return f"服务器错误: {str(e)}", 500
@app.route('/results/<job_id>/<filename>')
def serve_results_file(job_id, filename):
"""提供任务结果文件"""
try:
task_output_dir = os.path.join(app.config['OUTPUT_FOLDER'], job_id)
if not os.path.exists(task_output_dir):
return f"任务目录不存在: {job_id}", 404
file_path = os.path.join(task_output_dir, filename)
if not os.path.exists(file_path):
return f"请求的文件不存在: {filename}", 404
logger.info(f"提供结果文件: {file_path}")
return send_from_directory(task_output_dir, filename)
except Exception as e:
logger.error(f"提供结果文件时出错: {str(e)}")
return f"服务器错误: {str(e)}", 500
@app.route('/api/upload', methods=['POST'])
def upload_file():
"""处理文件上传"""
try:
# 检查是否有文件
if 'file' not in request.files:
logger.error("上传请求中没有文件")
return jsonify({'success': False, 'message': '没有找到文件'}), 400
file = request.files['file']
# 检查文件名是否为空
if file.filename == '':
logger.error("上传文件名为空")
return jsonify({'success': False, 'message': '空文件名'}), 400
# 检查文件类型
if not allowed_file(file.filename):
logger.error(f"不支持的文件类型: {file.filename}")
return jsonify({'success': False, 'message': f'不支持的文件类型,请上传 {", ".join(ALLOWED_EXTENSIONS)} 格式的视频'}), 400
# 检查磁盘空间
free_space = get_free_space_gb(UPLOAD_FOLDER)
if free_space < 5: # 少于5GB空间警告
logger.warning(f"磁盘空间不足: {free_space:.2f}GB")
return jsonify({'success': False, 'message': f'磁盘空间不足,仅剩 {free_space:.2f}GB'}), 400
# 生成安全的文件名
filename = secure_filename(file.filename)
# 生成任务ID
task_id = str(uuid.uuid4())
# 创建上传路径
upload_dir = os.path.join(app.config['UPLOAD_FOLDER'], task_id)
os.makedirs(upload_dir, exist_ok=True)
# 保存文件
file_path = os.path.join(upload_dir, filename)
file.save(file_path)
logger.info(f"文件 {filename} 上传成功,路径: {file_path}")
# 创建任务
current_time = time.strftime('%Y-%m-%d %H:%M:%S')
tasks[task_id] = {
'id': task_id,
'filename': filename,
'original_filename': file.filename,
'status': 'pending',
'message': '等待处理',
'progress': 0,
'create_time': current_time,
'update_time': current_time,
'file_path': file_path
}
save_task_status()
# 异步处理视频
executor.submit(process_video_async, task_id, file_path, filename)
# 返回任务ID
return jsonify({
'success': True,
'message': '文件上传成功,开始处理',
'task_id': task_id
})
except Exception as e:
logger.error(f"上传处理失败: {str(e)}")
try:
logger.error(traceback.format_exc())
except Exception:
pass
return jsonify({'success': False, 'message': f'上传失败: {str(e)}'}), 500
@app.route('/api/tasks', methods=['GET'])
def get_all_tasks():
"""获取所有任务状态"""
return jsonify({'tasks': list(tasks.values())})
@app.route('/api/tasks/<task_id>', methods=['GET'])
def get_task_status(task_id):
"""获取特定任务状态"""
if task_id in tasks:
return jsonify({'task': tasks[task_id]})
else:
return jsonify({'success': False, 'message': '任务不存在'}), 404
@app.route('/api/tasks/<task_id>/cancel', methods=['POST'])
def cancel_task(task_id):
"""取消任务"""
try:
if task_id not in tasks:
return jsonify({'success': False, 'message': '任务不存在'}), 404
task = tasks[task_id]
# 只能取消待处理或处理中的任务
if task['status'] not in ['pending', 'processing']:
return jsonify({'success': False, 'message': f'无法取消状态为 {task["status"]} 的任务'}), 400
# 标记任务为已取消
task['status'] = 'cancelled'
task['message'] = '任务已取消'
task['update_time'] = time.strftime('%Y-%m-%d %H:%M:%S')
save_task_status()
# 注意:这里没有实际终止正在运行的处理过程
# 如果需要,应该实现一个线程安全的取消机制
return jsonify({'success': True, 'message': '任务已取消'})
except Exception as e:
logger.error(f"取消任务失败: {str(e)}")
return jsonify({'success': False, 'message': f'取消任务失败: {str(e)}'}), 500
@app.route('/api/tasks/<task_id>', methods=['DELETE'])
def delete_task(task_id):
"""删除任务和相关文件"""
try:
if task_id not in tasks:
return jsonify({'success': False, 'message': '任务不存在'}), 404
# 删除上传文件
upload_dir = os.path.join(app.config['UPLOAD_FOLDER'], task_id)
if os.path.exists(upload_dir):
try:
shutil.rmtree(upload_dir)
logger.info(f"删除上传文件目录: {upload_dir}")
except Exception as e:
logger.error(f"删除上传文件目录失败: {str(e)}")
# 删除输出文件
output_dir = os.path.join(app.config['OUTPUT_FOLDER'], task_id)
if os.path.exists(output_dir):
try:
shutil.rmtree(output_dir)
logger.info(f"删除输出文件目录: {output_dir}")
except Exception as e:
logger.error(f"删除输出文件目录失败: {str(e)}")
# 从任务列表中删除
del tasks[task_id]
save_task_status()
return jsonify({'success': True, 'message': '任务已删除'})
except Exception as e:
logger.error(f"删除任务失败: {str(e)}")
return jsonify({'success': False, 'message': f'删除任务失败: {str(e)}'}), 500
@app.route('/api/tasks/<task_id>/retry', methods=['POST'])
def retry_task(task_id):
"""重试任务"""
try:
if task_id not in tasks:
return jsonify({'success': False, 'message': '任务不存在'}), 404
task = tasks[task_id]
# 只能重试失败或取消的任务
if task['status'] not in ['failed', 'cancelled']:
return jsonify({'success': False, 'message': f'无法重试状态为 {task["status"]} 的任务'}), 400
# 检查文件是否存在
if not os.path.exists(task['file_path']):
return jsonify({'success': False, 'message': '原始文件不存在,无法重试'}), 400
# 更新任务状态
task['status'] = 'pending'
task['message'] = '等待重新处理'
task['progress'] = 0
task['update_time'] = time.strftime('%Y-%m-%d %H:%M:%S')
# 删除旧的输出目录(如果存在)
output_dir = os.path.join(app.config['OUTPUT_FOLDER'], task_id)
if os.path.exists(output_dir):
try:
shutil.rmtree(output_dir)
logger.info(f"清理旧的输出目录: {output_dir}")
except Exception as e:
logger.error(f"清理旧的输出目录失败: {str(e)}")
save_task_status()
# 重新提交处理任务
executor.submit(process_video_async, task_id, task['file_path'], task['filename'])
return jsonify({'success': True, 'message': '任务已重新提交'})
except Exception as e:
logger.error(f"重试任务失败: {str(e)}")
return jsonify({'success': False, 'message': f'重试任务失败: {str(e)}'}), 500
if __name__ == "__main__":
try:
logger.info("正在启动服务器...")
logger.info(f"工作目录: {os.getcwd()}")
logger.info(f"静态文件目录: {os.path.abspath('static')}")
logger.info(f"模板文件目录: {os.path.abspath('templates')}")
if not os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')):
logger.error("模板目录不存在!")
if not os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates', 'index.html')):
logger.error("index.html 不存在!")
app.run(host='0.0.0.0', port=5001, debug=False)
except Exception as e:
logger.error(f"服务器启动失败: {str(e)}")
logger.error(traceback.format_exc())