567 lines
22 KiB
Python
567 lines
22 KiB
Python
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()) |