From 6de1b4d12ada9338b80eda329009f94d3c8ecf27 Mon Sep 17 00:00:00 2001 From: jcy Date: Wed, 14 May 2025 12:25:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E8=87=B3?= =?UTF-8?q?=20=E6=9C=8D=E5=8A=A1=E5=99=A8=E5=90=AF=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 服务器启动/server.py | 567 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 567 insertions(+) create mode 100644 服务器启动/server.py diff --git a/服务器启动/server.py b/服务器启动/server.py new file mode 100644 index 0000000..b977e8b --- /dev/null +++ b/服务器启动/server.py @@ -0,0 +1,567 @@ +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""" + + + + + 简化处理报告 + + + +

简化视频处理报告

+

视频文件: {os.path.basename(video_path)}

+

注意: 这是一个简化的处理报告,完整功能未启用。

+ + + """) + + 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""" + + + + + 处理状态 + + + +

视频处理完成

+
+

视频已成功处理,但未找到标准报告文件。

+

请检查服务器日志以获取详细信息。

+

处理时间: {round(end_time - start_time, 2)}秒

+
+ + + """) + 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/') +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/') +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//') +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/', 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//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/', 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//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()) \ No newline at end of file