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

视频处理完成

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

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

处理时间: {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) 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/', 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//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/', 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//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)